Last active
November 4, 2021 14:55
-
-
Save samhernandez/270a1f11a786b777d64da06650b5fdd3 to your computer and use it in GitHub Desktop.
Upload base64 image from front-end profile form with CraftCMS
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 | |
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