Sulu: Is there a way to create images programatically - sulu

Is there a way in Sulu CMS to create an image object programmatically without uploading an actual image through the admin interface?
The usecase is, that I would like to display a resized fallback image, if the user does not upload a header image.

You can use the sulu_media.media_manager service and give it a new UploadedFile instance e.g.:
$mediaManager->save(
new UploadedFile($path, $fileName, $mimeType),
[
'title' => 'Test',
'locale' => 'de',
'description' => '',
'collection' => $collectionId,
// ...
]
);
If performance matters or you need to import many files you should create the entities (media, files, fileVersion, fileVersionMeta) yourself and use the sulu.media.storage service to save the actually file which will return you the storageOptions e.g.:
$media = new \Media();
$file = new File();
$file->setVersion(1);
$file->setMedia($media);
$media->addFile($file);
$mediaType = $this->entityManager->getReference(
MediaType::class, $this->mediaTypeManager->getMediaType($this->getMimeType($uploadedFile))
);
$media->setType($mediaType);
$collection = $this->entityManager->getReference(Collection::class, $collectionid);)
$media->setCollection($collection);
$storageOptions = $this->mediaStorage->save($file->getPathname(), $fileName)
$fileVersion = new FileVersion();
$fileVersion->setVersion($file->getVersion());
$fileVersion->setSize($uploadedFile->getSize());
$fileVersion->setName($fileName);
$fileVersion->setStorageOptions($storageOptions);
$fileVersion->setMimeType(/* ... */);
$fileVersion->setFile($file);
$file->addFileVersion($fileVersion);
$fileVersionMeta = new FileVersionMeta();
$fileVersionMeta->setTitle($title);
$fileVersionMeta->setDescription('');
$fileVersionMeta->setLocale($locale);
$fileVersionMeta->setFileVersion($fileVersion);
$fileVersion->addMeta($fileVersionMeta);
$fileVersion->setDefaultMeta($fileVersionMeta);
$this->entityManager->persist($fileVersionMeta);
$this->entityManager->persist($fileVersion);
$this->entityManager->persist($file);
$this->entityManager->persist($media);
// after importing the files or after every 100 files you should flush the entitymanager
$this->entityManager->flush();
// I also recommend in a import doing a clear to keep the entitymanager unitofwork small as possible
$this->entityManager->clear();
The media type manager is available under sulu_media.type_manager and the doctrine entityManager doctrine.orm.entity_manager

In the end we made it by utilizing an image proxy (Thumbor in our case). This frees us from the image format restrictions of sulu and allows us to generate exactly the scaled / cropped versions of the file, that we need - all based on the original image url, that has been used for the upload.

Related

TYPO3 ConnectionPool find a file after the uid of the file reference and update data

The concept is that, after a successfull save of my object, it should update a text in the database (With a Hook). Lets call the field 'succText'. The table i would like to access is the sys_file but i only get the sys_file_reference id when i save the object. So i thought i could use the ConnectionPool to select the sys_file row of this file reference and then insert the data on the field 'succText'.
I tried this:
public function processDatamap_preProcessFieldArray(array &$fieldArray, $table, $id, \TYPO3\CMS\Core\DataHandling\DataHandler &$pObj) {
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_file_reference');
$findItemsId = $queryBuilder
->select('*')
->from('sys_file_reference')
->join(
'sys_file_reference',
'sys_file',
'reference',
$queryBuilder->expr()->eq('reference.uid', $queryBuilder->quoteIdentifier('uid_local'))
)
->where(
$queryBuilder->expr()->eq('uid_local', $queryBuilder->createNamedParameter($fieldArray['downloads'], \PDO::PARAM_INT))
)
->execute();
}
But this give me back the sys_file_reference id and not the id and the field values of the sys_file table.
As for the update, i havent tried it yet, cause i haven't figured out yet, how to get the row that needs to be updated. I gues with a subquery after the row is found, i don't really know.
The processDatamap_preProcessFieldArray is going to be renamed to post. I only have it this way in order to get the results on the backend.
Thanks in advance,
You might want to make use of the FileRepository class here.
$fileRepository = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Resource\FileRepository::class);
$fileObjects = $fileRepository->findByRelation('tablename', 'fieldname', $uid);
Where $uid is the ID of the record that the files are connected to via file reference.
You will get back an array of file objects to deal with.
I resolved my problem by removing the first code and adding a filerepository instance.
$fileRepository = GeneralUtility::makeInstance(FileRepository::class);
$fileObjects = $fileRepository->findByRelation('targetTable', 'targetField', $uid);
VERY IMPORTANT!
If you are creating a new element then TYPO3 assigns a temp UID variable with a name that looks like this NEW45643476. In order to get the $uid from the processDatamap_afterDatabaseOperations you need to add this code before you get the instance of the fileRepository.
if (GeneralUtility::isFirstPartOfStr($uid, 'NEW')) {
$uid = $pObj->substNEWwithIDs[$uid];
}
Now as far as the text concerns, i extracted from a pdf. First i had to get the basename of the file in order to find its storage location and its name. Since i have only one file i don't really need a foreach loop and i can use the [0] as well. So the code looked like this:
$fileID = $fileObjects[0]->getOriginalFile()->getProperties()['uid'];
$fullPath[] = [PathUtility::basename($fileObjects[0]->getOriginalFile()->getStorage()->getConfiguration()['basePath']), PathUtility::basename($fileObjects[0]->getOriginalFile()->getIdentifier())];
This, gives me back an array looking like this:
array(1 item)
0 => array(2 items)
0 => 'fileadmin' (9 chars)
1 => 'MyPdf.pdf' (9 chars)
Now i need to save the text from every page in a variable. So the code looks like this:
$getPdfText = '';
foreach ($fullPath as $file) {
$parser = new Parser();
$pdf = $parser->parseFile(PATH_site . $file[0] . '/' . $file[1]);
$pages = $pdf->getPages();
foreach ($pages as $page) {
$getPdfText .= $page->getText();
}
}
Now that i have my text i want to add it on the database so i will be able to use it on my search action. I now use the connection pool to get the file from the sys_file.
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_file');
$queryBuilder
->update('sys_file')
->where(
$queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($fileID))
)
->set('pdf_text', $getPdfText)
->execute();
Now everytime i choose a PDF from my extension, i save its text on the database.
EXTRA CONTENT
If you want to include the PDFParser as well and you are on composer mode, then add this on your composer.json:
"smalot/pdfparser" : "*"
and on the autoload:
"Smalot\\PdfParser\\" : "Packages/smalot/pdfparser/src/"
Then under: yourExtension/Classes/Hooks/DataHandler.php add the namespace:
use Smalot\PdfParser\Parser;
Now you are able to use the getPages() and getText() functions.
The Documentation
If i missed something let me know and i will add it.

How to get images in the product list view with an API call in Shopware?

I’m working on a single-page-application with fully client-side rendered frontend (React) and Shopware acting as a headless CMS in the background. So all product-data will be pulled from the API and checkout will also use the API to send the data.
My problem is the following:
When trying to render the product list page, I call the articles endpoint which returns the basic info of all my products. The problem is that I would also need to render the main image associated with each product and this list does not expose any data from the media attached to the products.
I can get the media item(s) for a product using the Media endpoint, problem is that this one expects a Media Id which I cannot get from the listed representations of the articles.
So right now the only way I see to get the images is first making a call that gets all the products, then loop through them and get each product’s details, calling the article endpoint again with each product ID, then when I have the media IDs I loop through those and get the images for each product using the media endpoint, because that’s the only one that exposes the actual image path in the response. This seems way too complicated and slow.
Is there a smarter way to do this? Can I get Shopware to output the 1st image’s path in the Article list response, that’s associated with the current product?
Also I saw that the paths to the images look like this:
/media/image/f5/fb/95/03_200x200.jpg
The first part up to /media/image/ is fixed, that’s straight forward, and I get the image’s filename and extension in the article detail’s response in the media object and even the file extension, which looks like this in the API's response.
The problem is that I don’t know what’s the f5/fb/95/ stand for. If I could get this info from the details I could assemble the URL for the image programmatically and then I wouldn’t need the call to the media endpoint.
You can see how the path is generated in the class \Shopware\Bundle\MediaBundle\Strategy\Md5Strategy of Shopware's source code. The two relevant methods are:
<?php
public function normalize($path)
{
// remove filesystem directories
$path = str_replace('//', '/', $path);
// remove everything before /media/...
preg_match("/.*((media\/(?:archive|image|music|pdf|temp|unknown|video|vector)(?:\/thumbnail)?).*\/((.+)\.(.+)))/", $path, $matches);
if (!empty($matches)) {
return $matches[2] . '/' . $matches[3];
}
return $path;
}
public function encode($path)
{
if (!$path || $this->isEncoded($path)) {
return $this->substringPath($path);
}
$path = $this->normalize($path);
$path = ltrim($path, '/');
$pathElements = explode('/', $path);
$pathInfo = pathinfo($path);
$md5hash = md5($path);
if (empty($pathInfo['extension'])) {
return '';
}
$realPath = array_slice(str_split($md5hash, 2), 0, 3);
$realPath = $pathElements[0] . '/' . $pathElements[1] . '/' . join('/', $realPath) . '/' . $pathInfo['basename'];
if (!$this->hasBlacklistParts($realPath)) {
return $realPath;
}
foreach ($this->blacklist as $key => $value) {
$realPath = str_replace($key, $value, $realPath);
}
return $realPath;
}
Step for step, this happens generally to an image path, for example https://example.com/media/image/my_image.jpg, upon passing it to the encode-method:
The normalize method strips everything except for media/image/my_image.jpg
An md5-hash is generated out of the resulting string: 5dc18cdfa0...
The resulting string from step 1 is split at every /: ['media', 'image', 'my_image.jpg']
The first three character pairs from the md5-hash are being stored into an array: ['5d', 'c1', '8c']
The final path is being assembled out of the arrays from steps 3 and 4: media/image/5d/c1/8c/my_image.jpg
Every occurence of /ad/ is being replaced by /g0/. This is done, because there are ad-blockers which block requests to URLs containing /ad/
Shopware computes the path for you.
There is acutally no need to know about the hashed path...
You will be redirected to the correct path.
Try the following:
/media/image/ + image['path'] + image['extension']
An for the thumbnails:
/media/image/thumbnail/ + image['path'] + '_200x200' + image['extension']
or
/media/image/thumbnail/ + image['path'] + '_600x600' + image['extension']

Zend_Translate Strategy for a huge grown web site

since I'm thinking in a good way to handle translation did part of a implementation and going toward a concept that still don't know if it's good and I would like to share it and get the pros and cons of it with someone that think it's a good point to explore.
The architecture is meant to work in a componentized site with translations comming from Actions, Forms, Views, View_Helpers and even Action_Helpers.
The ideis is simple:
Zend_Translate will the got from registry in every component and will receive __FILE__ as parameter. Since it was initialized with 'clear' on bootstrap it will be possible to load just the array file that correspont to this calling compoment. When comes to the missing translations they will be logged to a database (to avoid log duplicates) and/or be added to the corresponding array file in the remaining untranslated languages (as well as have the array file created) with a null value where it's is not set yet.
My guess is that using cache and specializing Translate i can ignore the translations that are set with null (by the addition made before) without log it again (displayin just the key) it will overhead a little bit the firt call for a large untraslated page and then gain performance later as well as maintainability and work ability with the automation of the translation process that would like to supply for the user.
But after that I was figuring out that I could build a array with the missing translations from every component to be save at the request end, and that is my question.
Had you folks had some experience with this that could be helpful to determine what's the best strategy?
bootstrap
protected function _initLocale() {
$translateSession = new Zend_Session_Namespace('translate');
$locale = isset($translateSession->locale) ? $translateSession->locale : 'auto';
try {
$zendLocale = new Zend_Locale($locale);
} catch (Zend_Locale_Exception $e) {
$zendLocale = new Zend_Locale('en_US');
}
Zend_Registry::set('Zend_Locale', $zendLocale);
$translate = new Engine_Translate('customarray', array('clear'));
$logger = Engine_Logger::getLogger();
$translate->setOptions( array('log2db' => $logger ,'log' => $logger, 'logPriority' => Zend_Log::ALERT, 'logUntranslated' => true));
Zend_Registry::set('Zend_Translate', $translate);
}
simple library
function getAvailableTranslationLanguages() {
return array("pt_BR"=>"Português","en_US"=>"Inglês");
}
function setTranslationLanguage($code) {
$translateSession = new Zend_Session_Namespace('translate');
$translateSession->locale = $code;
}
function getTranslator($file) {
$relative = str_replace(APPLICATION_PATH, '', $file);
$code = Zend_Registry::get('Zend_Locale');
$path = APPLICATION_PATH . '\\lang\\' . $code . $relative;
$translator = Zend_Registry::get('Zend_Translate');
try {
$translator->addTranslation($path, $code);
} catch (Exception $e) {
createTranslationFile($path);
}
return $translator;
}
function createTranslationFile($path) {
if(!file_exists(dirname($path)))
mkdir(dirname($path), 0777, true);
$file = fopen($path, 'w');
if($file) {
$stringData = "<?php\n return array(\n );";
fwrite($file, $stringData);
fclose($file);
} else {
$logger = Engine_Logger::getLogger();
$logger->info(Engine_Logger::get_string('ERRO ao abrir arquivo de tradução: ' . $path));
}
}
The use
class App_Views_Helpers_Loginbox extends Zend_View_Helper_Abstract
{
public function loginbox() {
$translate = getTranslator(__FILE__);
Translation resources
If I understand correctly, you want to create new adapter for each action helper/ view helper/ etc. This is IMO wrong and hugely ineffective. I would stick the translations to URLs. Make one common.php for translations used everywhere, module.php for module-specific and page-name.php for page specific translation. Then array_merge them together and create one adapter in Bootstrap. Then cache it (using URL as the cache key) somewhere - prefferably in memory (=memcached, apc). That way you would create the translate adapter from cache very effectively - only load+unserialize. Many translations (for each helper) means many disc accesses, means lower speed and scalability as disc will soon be the bottleneck.

Set values to form builds by zend framework

I use Zend framework to build the forms, I want to make the edit action, since the user click on edit, the form appears with users data, how can I set the data to the form which is built dynamically??
$form->populate($data);
where $data is an array of key value pairs containing your data.
$form = new Zend_Form;
if ($this->_request->isPost()) {
//to just populate
$form->populate($this->_getAllParams());
//or auto populate during validation
if ($form->isValid($this->_getAllParams()) {
//do stuff if valid
}
}
I know that ZF maunal is pretty messy and not everything is clear, but I think forms are explained pretty nice (with examples). You should research more by yourself.
http://framework.zend.com/manual/en/zend.form.html
$Menu = new Admin_Model_DbTable_Menu();
$row = $Menu->fetchRow($Menu->select()->where('id = ?', $id));
$Addmenu = new Admin_Form_Addmenu();
$Addmenu->populate($row->toArray());

Zend Framework email content generation

With Zend_Framework, I wondered what is considered best practice for building up the content to send in a HTML email. In my case the content of the email that is sent is determined by a number of factors, for example the number of returned rows for a specific value in the database. Because of this it makes sense for me that the content is built up within the controller that sends the email which talks to the relevant database models and determines what the content should be. Where i'm not sure this works is that our designers and copyrighters will often want to adjust the copy in emails and this would then require them to make changes to a model or ask me to. Should i be handling this differently? Should i perhaps be storing HTML snippets somewhere containing the different text and then calling these somehow?
EDIT following from the answer by fireeyedboy, would it be acceptable to do something like this. Create a folder inside views called "partials" and use this to store text/html snippets that i can then call in where i need and replace special strings with dynamic values using regexp(or similar).
$nview = new Zend_View();
$nview->setScriptPath(APPLICATION_PATH.'/views/partials/');
$bodytext = $nview->render('response.phtml');
$mail = new Zend_Mail();
$mail->setBodyText($bodytext);
// etc ...
e.g. in this context where two different templates could be used depending on variables returned from a Model:
// within a controller
public function emailAction()
{
$images = new Model_ApplicationImages();
$totimages = count($images->fetchImages($wsid));
$acceptedImages = $images->fetchImages($wsid,'approved');
$accepted = count($acceptedImages);
$rejectedImages = $images->fetchImages($wsid,'rejected');
$rejected = count($rejectedImages);
$response = ($rejected == $totimages)?'rejected':'approved';
$nview = new Zend_View();
$nview->setScriptPath(APPLICATION_PATH.'/views/partials/');
$content = $nview->render($response.'.phtml');
$mail = new Zend_Mail();
$mail->setBodyText($content);
// etc
}
Is there a more elegant way i can/should be doing this?
Not sure if this is best practice, but what I did is extend Zend_Mail with methods like these:
setTemplatePath( $templatePath );
setTemplateHtml( $templateHtml );
setTemplateText( $templateText );
setTemplateArguments( array $templateArguments );
...then at some point in my overwrittensend() I do:
$view = new Zend_View();
$view->setScriptPath( $this->_templatePath );
foreach( $this->_templateArguments as $key => $value )
{
$view->assign( $key, $value );
}
if( null !== $this->_templateText )
{
$bodyText = $view->render( $this->_templateText );
$this->setBodyText( $bodyText );
}
if( null !== $this->_templateHtml )
{
$bodyHtml = $view->render( $this->_templateHtml );
$this->setBodyHtml( $bodyHtml );
}
So to utilize this you would do something like:
$mail = new My_Extended_Zend_Mail();
$mail->setTemplatePath( 'path/to/your/mail/templates' );
$mail->setTemplateHtml( 'mail.html.phtml' );
$mail->setTemplateText( 'mail.text.phtml' );
$mail->setTemplateArguments(
'someModel' => $someFunkyModel,
/* etc, you get the point */
)
$mail->send();
In other words, with this you can let your designers and copywriters simply edit views (templates) like they are used to already. Hope this helps and has inspired you to come up with something funky that suits your needs.
PS:
Since you mention arbitrary data rows, you can, for instance, utilize the partialLoop view helper that comes with ZF for this. But you probably were aware of this already?
PPS:
I actually agree with chelmertz' comment about not extending Zend_Mail but wrapping it in my own component.