Skip to content

Instantly share code, notes, and snippets.

@luispabon
Last active July 30, 2022 11:42
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save luispabon/32b97a4a7cad5115b269519467434763 to your computer and use it in GitHub Desktop.
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)
<?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;
}
<?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);
}
}
<?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>
# 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
{# 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 %}
# 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%"
@luispabon
Copy link
Author

luispabon commented Mar 21, 2017

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment