Last active
July 30, 2022 11:42
-
-
Save luispabon/32b97a4a7cad5115b269519467434763 to your computer and use it in GitHub Desktop.
Easyadmin bundle + TinyMCE + Base64 images stored into s3 (will load up TinyMCE on all your textareas)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace AppBundle\Entity; | |
/** | |
* Interface that identifies entities which have fields that can contain base64 encoded images | |
* | |
* @package AppBundle\Entity | |
*/ | |
interface Base64EncodedImagesInterface | |
{ | |
/** | |
* Return a list of fields base64 encoded images can be on. EG ['body', 'intro']. | |
* | |
* @return array | |
*/ | |
public function getFields(): array; | |
/** | |
* Return the prefix any images may be stored into. | |
* | |
* @return string | |
*/ | |
public function getStoragePrefix(): string; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace AppBundle\Event; | |
use AppBundle\Entity\Base64EncodedImagesInterface; | |
use Aws\S3\S3ClientInterface; | |
use Doctrine\Common\EventSubscriber; | |
use Doctrine\ORM\Event\LifecycleEventArgs; | |
use Doctrine\ORM\Event\PreUpdateEventArgs; | |
use Doctrine\ORM\Events; | |
/** | |
* Extracts base64 encoded image links from an entity, uploads to s3 and changes the entity to use the public link. | |
* | |
* @copyright Auron Consulting Ltd | |
*/ | |
class Base64ImagesSubscriber implements EventSubscriber | |
{ | |
/** | |
* @var S3ClientInterface | |
*/ | |
private $s3; | |
/** | |
* @var string | |
*/ | |
private $bucket; | |
public function __construct(S3ClientInterface $s3, string $bucket) | |
{ | |
$this->s3 = $s3; | |
$this->bucket = $bucket; | |
} | |
/** | |
* Returns an array of events this subscriber wants to listen to. | |
* | |
* @return array | |
*/ | |
public function getSubscribedEvents() | |
{ | |
return [ | |
Events::prePersist, | |
Events::preUpdate, | |
]; | |
} | |
/** | |
* Handle creation of items. | |
* | |
* @param LifecycleEventArgs $args | |
*/ | |
public function prePersist(LifecycleEventArgs $args) | |
{ | |
$this->handle($args->getEntity()); | |
} | |
/** | |
* Handle update of items. | |
* | |
* @param PreUpdateEventArgs $args | |
*/ | |
public function preUpdate(PreUpdateEventArgs $args) | |
{ | |
$this->handle($args->getEntity()); | |
} | |
/** | |
* If the entity is supported, inspects the required fields and extracts any base64 encoded | |
* images on image links, uploads them to s3 and updates the entity field with the new links. | |
* | |
* @param mixed $entity | |
*/ | |
private function handle($entity) | |
{ | |
$pattern = '/src=\"data:image\/([a-zA-Z]*);base64,([^\"]*)\"/'; | |
if ($entity instanceof Base64EncodedImagesInterface) { | |
$folder = $entity->getStoragePrefix(); | |
$callback = function ($match) use ($folder) { | |
if (count($match) === 3) { | |
$type = $match[1]; | |
$base64Data = $match[2]; | |
if ($type === 'jpeg') { | |
$type = 'jpg'; | |
} | |
$filename = sprintf('%s.%s', md5($base64Data), $type); | |
return sprintf('src="%s"', $this->upload($filename, $folder, $base64Data)); | |
} | |
}; | |
foreach ($entity->getFields() as $field) { | |
$getter = sprintf('get%s', ucfirst($field)); | |
$setter = sprintf('set%s', ucfirst($field)); | |
$entity->{$setter}(preg_replace_callback($pattern, $callback, $entity->{$getter}())); | |
} | |
} | |
} | |
/** | |
* Uploads a base64 encoded stream into s3 to the given filename and returns its public url. | |
* | |
* @param string $filename | |
* @param string $folder | |
* @param string $base64Data | |
* | |
* @return string | |
*/ | |
private function upload(string $filename, string $folder, string $base64Data): string | |
{ | |
$key = sprintf('%s/%s', $folder, $filename); | |
$this->s3->upload($this->bucket, $key, base64_decode($base64Data)); | |
return $this->s3->getObjectUrl($this->bucket, $key); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?xml version="1.0" encoding="UTF-8"?> | |
<!-- You're gonna need to add this CORS policy to your s3 bucket to let TinyMCE manipulate images already stored in here --> | |
<CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/"> | |
<CORSRule> | |
<!-- Add in though your real origins here --> | |
<AllowedOrigin>http://*</AllowedOrigin> | |
<AllowedOrigin>https://*</AllowedOrigin> | |
<AllowedMethod>GET</AllowedMethod> | |
<AllowedMethod>HEAD</AllowedMethod> | |
<MaxAgeSeconds>3000</MaxAgeSeconds> | |
<ExposeHeader>Access-Control-Allow-Origin</ExposeHeader> | |
<AllowedHeader>Access-Control-Allow-Origin</AllowedHeader> | |
</CORSRule> | |
</CORSConfiguration> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# app/config/easy_admin.yml | |
# You need your complete config here and load this yml file up from your main config; or merge into config.yml or equivalent | |
parameters: | |
tinymce_api_key: 'Dont worry, tinymce will bug you to get this with the right URL for it' | |
easy_admin: | |
design: | |
assets: | |
js: | |
- 'https://cloud.tinymce.com/stable/tinymce.min.js?apiKey=%tinymce_api_key%' | |
entities: | |
# Your stuff |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{# app/Resources/views/easy_admin/layout.html.twig #} | |
{% extends '@EasyAdmin/default/layout.html.twig' %} | |
{% block body_javascript %} | |
<input name="image" type="file" id="upload" class="hidden" onchange=""> | |
<script> | |
// This will load up TinyMCE on all textareas, add a load of useful bits and pieces, allow you to upload images | |
// as base64 into the relevant textarea | |
$(function () { | |
tinymce.init({ | |
selector: "textarea", | |
height: 400, | |
plugins: [ | |
"advlist autolink lists link image imagetools charmap print preview hr anchor pagebreak", | |
"searchreplace wordcount visualblocks visualchars code fullscreen", | |
"insertdatetime media nonbreaking save table contextmenu directionality", | |
"emoticons template paste textpattern" | |
], | |
toolbar1: "undo redo | fullscreen | styleselect | bold italic | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | link media image", | |
toolbar2: "", | |
paste_data_images: true, | |
image_caption: false, | |
image_title: true, | |
automatic_uploads: true, | |
imagetools_cors_hosts: ['amazonaws.com', 's3-eu-west-1.amazonaws.com', 'localhost'], | |
file_picker_types: 'image', | |
imagetools_toolbar: "rotateleft rotateright | flipv fliph | editimage imageoptions", | |
setup: function (editor) { | |
// This ensures the actual field TinyMCE is supplanting gets updated with content. | |
editor.on('change', function () { | |
editor.save(); | |
}); | |
}, | |
image_advtab: true, | |
file_picker_callback: function (callback, value, meta) { | |
// This adds an extra button to the image menu and enables image uploads into the field as base64 | |
if (meta.filetype == 'image') { | |
var upload = $('#upload'); | |
upload.trigger('click'); | |
upload.on('change', function () { | |
var file = this.files[0]; | |
var reader = new FileReader(); | |
reader.onload = function (e) { | |
callback(e.target.result, { | |
alt: '' | |
}); | |
}; | |
reader.readAsDataURL(file); | |
}); | |
} | |
} | |
}); | |
}); | |
</script> | |
<style type="text/css"> | |
# Ensures TinyMCE full screen goes over EasyAdmin's furniture (except the save bar) | |
div.mce-fullscreen { | |
z-index: 4000 !important; | |
} | |
</style> | |
{% endblock %} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# app/config/services.yml | |
parameters: | |
aws_key: your | |
aws_secret: stuff | |
services: | |
base64_images_subscriber: | |
class: AppBundle\Event\Base64ImagesSubscriber | |
arguments: ["@s3_client", "%s3_bucket%"] | |
tags: | |
- { name: doctrine.event_subscriber, connection: default } | |
s3_client: | |
class: Aws\S3\S3Client | |
arguments: | |
- | |
version: '2006-03-01' | |
region: "eu-west-1" | |
credentials: | |
key: "%aws_key%" | |
secret: "%aws_secret%" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Did this for a small, personal project where I needed a little leeway to edit content on given pages, including uploading images and a little image manipulation (eg cropping and rotation), which TinyMCE supports nicely out of the box. Cobbled this up quickly with little in the way of checks, tests and dependency isolation since it's not out in the wild being used by strangers and I can afford being somewhat scruffy.
Had no end of trouble trying to get TinyMCE to manipulate images (cropping, rotating...) then pipe them back into the backend through the usual image upload mechanism - just wouldn't work, images would get dumped in base64'd and wouldn't trigger an upload.
The advantage of this roundabout approach though is that the backend side of things will work with any WYSIWYG editor that embeds images as Base64, including c&p into the editor.