Skip to content

Instantly share code, notes, and snippets.

@samhernandez
Last active November 4, 2021 14:55
Show Gist options
  • Save samhernandez/270a1f11a786b777d64da06650b5fdd3 to your computer and use it in GitHub Desktop.
Save samhernandez/270a1f11a786b777d64da06650b5fdd3 to your computer and use it in GitHub Desktop.
Upload base64 image from front-end profile form with CraftCMS
<?php
use craft\base\Element;
use craft\db\Query;
use craft\db\Table;
use craft\elements\Asset;
use craft\elements\Entry;
use craft\errors\ImageException;
use craft\helpers\Assets;
use craft\helpers\FileHelper;
use craft\helpers\Image;
use craft\helpers\StringHelper;
use craft\services\Entries;
use craft\web\Controller;
/**
* this is quick and dirty, probably not working code, but an example to get started.
* The use case is a front-end form that uses cropper.js with a Vue component. When
* an image is supplied that way, it is base64 encoded.
*/
class ProfileController extends Controller
{
/**
* @throws Throwable
*/
public function actionSave()
{
$profileId = Craft::$app->request->getBodyParam('id');
$profile = $profileId ?
Entry::find()->section('profiles')->anyStatus()->id($profileId)->one() :
new Entry(['section' => 'profiles']);
// assuming the entry has an asset field with handle `avatar`
$avatar = Craft::$app->request->getBodyParam('avatar');
// possibly deleting an avatar
if ($avatar === null) {
$profile->setFieldValue('avatar', []);
}
// they already had an avatar and the asset id was in the form
if (is_numeric($avatar)) {
$profile->setFieldValue('avatar', [(int)$avatar]);
}
// it's a base64 image from an image cropper or something
if ($this->isBase64Image($avatar)) {
$this->processBase64Avatar(
$profile,
'avatar',
'avatars',
$avatar
);
}
// continue setting up $profile then...
Craft::$app->elements->saveElement($profile);
}
protected function isBase64Image(string $base64Img = null): bool
{
return is_string($base64Img)
&& (bool)preg_match(
'/^data:((?<type>[a-z0-9]+\/[a-z0-9\+]+);)?base64,(?<data>.+)/i',
$base64Img
);
}
/**
* @throws Throwable
*/
protected function processBase64Avatar(Element $element, string $fieldHandle, string $volumeHandle, string $base64Img)
{
$matches = [];
if (!preg_match('/^data:((?<type>[a-z0-9]+\/[a-z0-9\+]+);)?base64,(?<data>.+)/i', $base64Img, $matches)) {
// (string) $element is the person's full name or the company name
Craft::error("Invalid base64 image for avatar on element " . get_class($element) . " (" . (string) $element . "): " . $base64Img);
return;
}
// the Vue.js profile forms create jpg from canvas, a safe default
$extension = '.jpg';
try {
$extension = FileHelper::getExtensionByMimeType($matches['type']);
} catch (InvalidArgumentException $e) { /* quiet */ }
// the filename will be readable like "joe-smith-dj88dugh.jpg"
$filename = StringHelper::slugify(
implode(' ', [
(string)$element, // is usually the `title` field
StringHelper::randomString(8)
])
).'.'.$extension;
$fileLocation = Assets::tempFilePath($extension);
$data = base64_decode($matches['data']);
FileHelper::writeToFile($fileLocation, $data);
try {
$this->setImageFieldOnElement(
$element,
$fieldHandle,
$volumeHandle,
$fileLocation,
$filename
);
} catch (\Throwable $e) {
if (file_exists($fileLocation)) {
FileHelper::unlink($fileLocation);
}
throw $e;
}
}
/**
* @throws \craft\errors\FileException
* @throws Throwable
* @throws \craft\errors\ElementNotFoundException
* @throws \yii\base\Exception
* @throws \craft\errors\AssetLogicException
*/
public function setImageFieldOnElement($element, $fieldHandle, $volumeHandle, $fileLocation, $filename)
{
$filename = Assets::prepareAssetName($filename ?? pathinfo($fileLocation, PATHINFO_BASENAME), true, true);
if (!Image::canManipulateAsImage(pathinfo($fileLocation, PATHINFO_EXTENSION))) {
throw new ImageException(Craft::t('app', 'User photo must be an image that Craft can manipulate.'));
}
$assetsService = Craft::$app->getAssets();
// If the photo exists, just replace the file.
$currentPhoto = $element->{$fieldHandle}->one();
if ($currentPhoto) {
$assetsService->replaceAssetFile($currentPhoto, $fileLocation, $filename);
return;
}
$volume = Craft::$app->getVolumes()->getVolumeByHandle($volumeHandle);
$folderId = (new Query())
->select(['id'])
->from([Table::VOLUMEFOLDERS])
->where(['volumeId' => $volume->id])
->scalar();
$photo = new Asset();
$photo->setScenario(Asset::SCENARIO_CREATE);
$photo->avoidFilenameConflicts = true;
$photo->tempFilePath = $fileLocation;
$photo->filename = $filename;
$photo->newFolderId = $folderId;
$photo->newLocation = "{folder:$folderId}$filename";
$photo->uploaderId = Craft::$app->getUser()->getId();
$photo->setVolumeId($volume->id);
$elementsService = Craft::$app->getElements();
$elementsService->saveElement($photo);
$element->setFieldValue($fieldHandle, [$photo->id]);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment