I'm starting to get into Zend Framework 2, and one of the things that I'd like to do is create an intercept that strips all the tabs out of template files before the view vars are inserted into them.
I gather that I'd have to implement my own render strategy, but I can't quite figure out how to replace the default one (phprenderer). Is it just a matter of setting a strategy of the same name with a higher value then the default one?
For reference, I've solved the issue by adding a Filter
namespace Application\Filter;
use Zend\Filter\FilterInterface;
class FilterMinifyHTML implements FilterInterface {
public function filter($value) {
return \Minify_HTML::minify($value, array(
'cssMinifier' => array('Minify_CSS', 'minify'),
'jsMinifier' => array('JSMin', 'minify'),
));
}
}
And then applied the filter as such (within PhpRenderStrategy.php)
/**
* Constructor
*
* #param PhpRenderer $renderer
*/
public function __construct(PhpRenderer $renderer) {
$this->renderer = $renderer;
$filterChain = new FilterChain();
$filterChain->attach(new FilterMinifyHTML());
$this->renderer->setFilterChain($filterChain);
}
Related
I'm not sure how to formulate the question, so feel free to edit it.
My current situation is as following:
I have a factory class which instantiates a form class. Dependency Injection (DI) is done via constructor injection. My problem is, that this form element has a Doctrine ObjectMultiCheckbox which requires a findby-method. For this findby-method I need the ID of a certain entity, but I cannot pass the ID through the factory class to the form.
My Question is, how can I deal with this situation? What is the best approach?
Let's say this is my factory class:
class CustomerFormFactory implements FactoryInterface
{
/**
* Create service
*
* #param ServiceLocatorInterface $serviceLocator
* #return Form
*/
public function createService(ServiceLocatorInterface $serviceLocator)
{
$em = $serviceLocator->get('Doctrine\ORM\EntityManager');
return new CustomerForm($em);
}
}
And I get the form via the service locator like this:
$customerForm = $this->getServiceLocator()->get('CustomerForm');
How can I pass the ID to the service locator? And if the form element requires a certain ID, doesn't it break the purpose of DI and services? Should I go for the "classic" way and instantiate the form element by myself like this:
$customerForm = new CustomerForm(EntityManager $em, int $id);
I'm really not sure what I should do or what is the best way to handle this.
In order to insert options into your form you could use the CreationOptions of the factory class.
So lets start by setting up our configurations for the FormElementManager (a serviceLocator for our Form Elements).
Within your Module.php:
use Zend\ModuleManager\Feature\FormElementProviderInterface;
class Module implements FormElementProviderInterface
{
// your module code
public function getFormElementConfig()
{
return [
'factories' => [
'myForm' => \Module\Form\MyFormFactory::class
]
];
}
}
After we've set up the configruation we should create our Factory, which returns the Form including it's dependencies. We also insert the options which we can re-use within our form class.
use Zend\ServiceManager\FactoryInterface;
use Zend\ServiceManager\MutableCreationOptionsTrait;
use Zend\ServiceManager\ServiceLocatorInterface;
class MyFormFactory implements FactoryInterface
{
use MutableCreationOptionsTrait;
/**
* Create service
*
* #param ServiceLocatorInterface $serviceLocator
*
* #return mixed
*/
public function createService(ServiceLocatorInterface $serviceLocator)
{
return new MyForm(
$serviceLocator->getServiceLocator()->get('Doctrine\ORM\EntityManager'),
'MyForm',
$this->getCreationOptions()
);
}
}
When using ZF3 it is better to use \Zend\ServiceManager\Factory\FactoryInterface instead of the \Zend\ServiceManager\FactoryInterface as this is the way ZF3 is going with using factories. In the example above I used the ZF2 (v2.7.6 zendframework/zend-servicemanager) version. See the comment on the class Zend\ServiceManager\FactoryInterface::class to replace it with the ZF3 version.
So now when we call ::get('myForm', ['id' => $id]) on the FormElementManager class you will get a MyForm instance and the options of the form will contain the options we've passed along.
So your form might look something similar:
class MyForm extends \Zend\Form\Form
{
public function __construct(
\Doctrine\Common\Persistence\ObjectManager $entityManager,
$name = 'myForm',
$options = []
) {
parent::__construct($name, $options);
$this->setEntityManager($entityManager);
}
public function init () {
/** add form elements **/
$id = $this->getOption('id');
}
}
You can also create the form and set the entityManager, but that is all up to you. You don't need to use constructor injection.
So an exmaple for your controller:
$myForm = $this->getServiceManager()->get('FormElementManager')->get('myForm', ['id' => 1337]);
$options = $myForm->getOptions();
// your options: ['id' => 1337]
You might not have the ServiceManager or Locator within your Controller as you're using ZF2.5+ or ZF3 so you've got to inject the FormElementManager or the Form class into your Controller by factory.
In case you don't have any other dependencies within your form but you want to set the options, you don't need to create a factory for each class. You can re-use the InvokableFactory::class as this will also inject the creationOptions.
I would like to create my own filter for Latte templating engine. There is an example in their documentation but it doesn't describe how to register it inside presenter.
$latte = new Latte\Engine;
$latte->addFilter('myFilter', function ($s) {
return someMagic($s)
});
I bet there will be simple way to get instance of Latte\Engine inside presenter but I'm not sure how.
Filters can be registered through config.neon too.
services:
nette.latteFactory:
setup:
- addFilter(abs, #App\Latte\AbsFilter)
- App\Latte\AbsFilter
Filter class can look like this:
namespace App\Latte;
class AbsFilter extends \Nette\Object
{
/**
* #param int $number
* #return int
*/
public function __invoke($number)
{
return abs($number);
}
}
In presenter, there is instance of Latte\Engine available in $this->template so everything you have to do is register filter like this:
<?php
abstract class BasePresenter extends Nette\Application\UI\Presenter
{
public function beforeRender()
{
// register filters
$this->template->addFilter('myFilter', function ($s) {
// don't forget to set your own magic
return someMagic($s);
});
}
}
?>
I postend an example using BasePresenter which is parent of all others presenters but you can register it only in presenter you want to and speed up your application.
In addition to #Nortys answer.
Sometimes it's usefull to inject some data from Presenter into anonymous function:
<?php
abstract class BasePresenter extends Nette\Application\UI\Presenter
{
public function beforeRender()
{
$locale = 'en';
// register filters
$this->template->addFilter('myFilter', function ($s) use ($locale) {
// don't forget to set your own magic
return someMagic($s, $locale);
});
}
}
?>
Nette 3.0 / 2020 Approach with Dependency Injection
Registering filters in control or presenter can lead to forgotten registration of filter. They should be registered once, at one place for whole application.
This approach is easy for extension, 1 new filter = 1 new class, no configuration, no touching other code. Apart other answers here, this one respects open-closed solid principle.
LatteFactory service with FilterProvider
First, we make a filter provide service:
interface FilterProviderInterface
{
public function getName(): string;
}
final class PlusFilterProvider implements FilterProviderInterface
{
public function __invoke(int $number, int $anotherNumber): int
{
return SomeStaticClass::plus($number, $anotherNumber);
}
public function getName(): string
{
return 'plus';
}
}
<?php
declare(strict_types=1);
namespace App\Latte;
use Latte\Engine;
use Latte\Runtime\FilterInfo;
final class LatteFactory
{
/**
* #var FilterProviderInterface[]
*/
private array $filterProviders;
/**
* #param FilterProviderInterface[] $filterProviders
*/
public function __construct(array $filterProviders)
{
$this->filterProviders = $filterProviders;
}
public function create(): Engine
{
$engine = new Engine();
// register your filters here
foreach ($this->filterProviders as $filterProvider) {
$engine->addFilter($filterProvider->getName(), $filterProvider);
}
return $engine;
}
}
How to add a new filter?
create new class that implements your interface FilterProviderInterface
register is as a service in your config, or better autodiscover it with search extension
that's it :)
Do you want to learn more?
I wrote more detail post How to Get Rid of Magic, Static and Chaos from Latte Filters where I explain the pros and cons of alternatives and why this one wins.
What's the "Zend" way of adding default variables to the ViewModel.
Currently I have:
return new ViewModel(array('form' => new CreateUserForm));
But I want to always add some variables to the ViewModel array. Like the time and date say, or categories for a menu. I was thinking of extending the ViewModel as that seems the OO way, but Zend always does things differently...
You could always extend the ViewModel if you want some extra functionality in there...
class MyViewModel extends ViewModel
{
/**
* Default Variables to set
*/
protected $_defaultValues = array(
'test' => 'bob'
);
/**
* Constructor
*
* #param null|array|Traversable $variables
* #param array|Traversable $options
*/
public function __construct($variables = null, $options = null)
{
//$variables = array_merge($this->_defaultValues, $variables);
$this->setVariables($this->_defaultValues);
parent::__construct($variables, $options)
}
}
Now in your controller just use return your new view model instead:
/**
* Some Controller Action
*/
function myAction()
{
return new MyViewModel();
}
One approach could be to have a method in your controller that returns ViewModel populated with time, date, etc. and then addVariables() to the returned model in the Action.
However, a better approach will be to use view helpers since they will be available in every view/layout throughout the application.
I'm using doctrine inheritance mapping to enable various objects to be linked to a comment entity. This is achieved through various concrete "Threads", which have a one-to-many relationship with comments. So taking a 'Story' element as an example, there would be a related 'StoryThread' entity, which can have many comments.
That is all working fine, but I'm having troubles trying to define a CommentAdmin class for the SonataAdminBundle that can be used as a child of the parent entities. For example, I'd want to be able to use routes such as:
/admin/bundle/story/story/1/comment/list
/admin/bundle/media/gallery/1/comment/list
Does anyone have any pointers about how I can go about achieving this? I'd love to post some code extracts but I haven't managed to find any related documentation so don't really know the best place to start.
I've been trying to use the SonataNewsBundle as a reference because they've implemented a similar parent/child admin relationship between posts and comments, but it appears as though this relies on the 'comment' (child) admin class to be hardcoded to know that it belongs to posts, and it also seems as though it needs to have a direct many-to-one relationship with the parent object, whereas mine is through a separate "Thread" entity.
I hope this makes sense! Thanks for any help.
Ok I managed to get this working eventually. I wasn't able to benefit from using the $parentAssociationMapping property of the CommentAdmin class, as the parent entity of a comment is a concrete instance of the Thread entity whereas the parent 'admin' class in this case is a Story (which is linked via the StoryThread). Plus this will need to remain dynamic for when I implement comments on other types of entity.
First of all, I had to configure my StoryAdmin (and any other admin classes that will have CommentAdmin as a child) to call the addChild method:
acme_story.admin.story:
class: Acme\Bundle\StoryBundle\Admin\StoryAdmin
tags:
- { name: sonata.admin, manager_type: orm, group: content, label: Stories }
arguments: [null, Acme\Bundle\StoryBundle\Entity\Story, AcmeStoryBundle:StoryAdmin]
calls:
- [ addChild, [ #acme_comment.admin.comment ] ]
- [ setSecurityContext, [ #security.context ] ]
This allowed me to link to the child admin section from the story admin, in my case from a side menu, like so:
protected function configureSideMenu(MenuItemInterface $menu, $action, Admin $childAdmin = null)
{
// ...other side menu stuff
$menu->addChild(
'comments',
array('uri' => $admin->generateUrl('acme_comment.admin.comment.list', array('id' => $id)))
);
}
Then, in my CommentAdmin class, I had to access the relevant Thread entity based on the parent object (e.g a StoryThread in this case) and set this as a filter parameter. This is essentially what is done automatically using the $parentAssociationMapping property if the parent entity is the same as the parent admin, which it most likely will be if you aren't using inheritance mapping. Here is the required code from CommentAdmin:
/**
* #param \Sonata\AdminBundle\Datagrid\DatagridMapper $filter
*/
protected function configureDatagridFilters(DatagridMapper $filter)
{
$filter->add('thread');
}
/**
* #return array
*/
public function getFilterParameters()
{
$parameters = parent::getFilterParameters();
return array_merge($parameters, array(
'thread' => array('value' => $this->getThread()->getId())
));
}
public function getNewInstance()
{
$comment = parent::getNewInstance();
$comment->setThread($this->getThread());
$comment->setAuthor($this->securityContext->getToken()->getUser());
return $comment;
}
/**
* #return CommentableInterface
*/
protected function getParentObject()
{
return $this->getParent()->getObject($this->getParent()->getRequest()->get('id'));
}
/**
* #return object Thread
*/
protected function getThread()
{
/** #var $threadRepository ThreadRepository */
$threadRepository = $this->em->getRepository($this->getParentObject()->getThreadEntityName());
return $threadRepository->findOneBy(array(
$threadRepository->getObjectColumn() => $this->getParentObject()->getId()
));
}
/**
* #param \Doctrine\ORM\EntityManager $em
*/
public function setEntityManager($em)
{
$this->em = $em;
}
/**
* #param \Symfony\Component\Security\Core\SecurityContextInterface $securityContext
*/
public function setSecurityContext(SecurityContextInterface $securityContext)
{
$this->securityContext = $securityContext;
}
An alternative to your code for direct related entities :
public function getParentAssociationMapping()
{
// we grab our entity manager
$em = $this->modelManager->getEntityManager('acme\Bundle\Entity\acme');
// we get our parent object table name
$className = $em->getClassMetadata(get_class($this->getParent()->getObject($this->getParent()->getRequest()->get('id'))))->getTableName();
// we return our class name ( i lower it because my tables first characted uppercased )
return strtolower( $className );
}
be sure to have your inversedBy variable matching the $className in order to properly work
Let's say I have a User Entity :
$user = new User(007);
echo $user->getName(); // display Bond
echo $user->getGender(); // display "Male";
echo $user->getDesignation() // display "Monsieur Bond" or "Mister Bond"
With this function :
public function getDesignation() {
if ($this->getGender() == 'Male') return "Monsieur ".$this->getName();
else return "Madame ".$this->getName();
}
How can I use the translator service inside this Entity to translate "Monsieur" and "Madame" ?
It seems the translator service should be used only inside a Controller, but I think it's appropriate in that case to use it inside this Entity.
The translator service is, like you say, a "service" you can use a service inside any class (i.e. defining it as a service too and using the dependency injector container). So, you can use the translator almost wherever you want.
But the entities like aldo said shouldn't have that responsability. In the worst scenario if you really want to translate things inside the entity, you could pass the translator to the entity with a set method, i.e.
$entity->setTranslator($translator);
but I recommend you too to create a class that handles the problem outside the entity, i.e. using the twig template
{{ entity.property|trans }}).
You shouldn't and in general it is not possible. According to the Single Responsibility Principle the entity have already their purpose, which is representing data on a database. Moreover the translation is a matter of representation, so it is unlikely that you want to address such a problem in the entity layer (unless you want to provide entities translated in different languages, which totally a different problem and shouldn't even be solved using the translator).
Rethink to your logic and try something different for this. Are you sure that you don't want to do this translation on the view layer? That would be the best thing probably. Otherwise (if your logic really need to have translation at a model level) you could create a wrapper class for entities and a factory to generate this "wrapped entities"; in that factory you could inject the translator service.
I ran into the similar problem and finally found this solution. This is not a direct answer to your problem because I'm also aware that an entity should have nothing to do with a service, like translator. So you should leave the getDesignation function untouched. Instead, in the presentation layer, twig for example, you translate that French designation.
<div>{% trans %}{{ entity.designation }}{% endtrans %} {{ entity.name }}</div>
And in your messages.en.yml
Monsieur: Mr.
Madame: Mrs.
I ran into this problem several times over the last years and always found a good enough workaround. This time my getIdentifyingName() methods that are heavily used across the whole project (like an explicit __toString()) had to translate some keywords used in the data layer, so there was no elegant workaround.
My solution this time is a TranslateObject and a corresponding helper service. The TranslateObject is a plain object holding a translation key and an array of placeholders which also can be TranslateObjects to allow multi level translation (like a getIdentifyingNameTranslateObject() calling another related object's getIdentifyingNameTranslateObject() within one of the placeholders):
namespace App\Utils;
class TranslateObject
{
/** #var string */
protected $transKey;
/** #var array */
protected $placeholders;
public function __construct(string $transKey, array $placeholders = [])
{
$this->transKey = $transKey;
$this->placeholders = self::normalizePlaceholders($placeholders);
}
public static function normalizePlaceholders(array $placeholders): array
{
foreach ($placeholders as $key => &$placeholder) {
if (substr($key, 0, 1) !== '%' || substr($key, -1, 1) !== '%') {
throw new \InvalidArgumentException('The $placeholder attribute must only contain keys in format "%placeholder%".');
}
if ($placeholder instanceof TranslateObject) {
continue;
}
if (is_scalar($placeholder)) {
$placeholder = ['value' => $placeholder];
}
if (!isset($placeholder['value']) || !is_scalar($placeholder['value'])) {
throw new \InvalidArgumentException('$placeholders[\'%placeholder%\'][\'value\'] must be present and a scalar value.');
}
if (!isset($placeholder['translate'])) {
$placeholder['translate'] = false;
}
if (!is_bool($placeholder['translate'])) {
throw new \InvalidArgumentException('$placeholders[\'%placeholder%\'][\'translate\'] must be a boolean.');
}
}
return $placeholders;
}
public function getTransKey(): string
{
return $this->transKey;
}
public function getPlaceholders(): array
{
return $this->placeholders;
}
}
The helper looks like this and does the work:
namespace App\Services;
use App\Utils\TranslateObject;
use Symfony\Contracts\Translation\TranslatorInterface;
class TranslateObjectHelper
{
/** #var TranslatorInterface */
protected $translator;
public function __construct(TranslatorInterface $translator)
{
$this->translator = $translator;
}
public function trans(TranslateObject $translateObject): string
{
$placeholders = $translateObject->getPlaceholders();
foreach ($placeholders as $key => &$placeholder) {
if ($placeholder instanceof TranslateObject) {
$placeholder = $this->trans($placeholder);
}
elseif (true === $placeholder['translate']) {
$placeholder = $this->translator->trans($placeholder['value']);
}
else {
$placeholder = $placeholder['value'];
}
}
return $this->translator->trans($translateObject->getTransKey(), $placeholders);
}
}
And then within the Entity there is a getIdentifyingNameTranslateObject() method returning a TranslateObject.
/**
* Get an identifying name as a TranslateObject (for use with TranslateObjectHelper)
*/
public function getIdentifyingNameTranslateObject(): TranslateObject
{
return new TranslateObject('my.whatever.translation.key', [
'%placeholder1%' => $this->myEntityProperty1,
'%placeholderWithANeedOfTranslation%' => [
'value' => 'my.whatever.translation.values.' . $this->myPropertyWithANeedOfTranslation,
'translate' => true,
],
'%placeholderWithCascadingTranslationNeeds%' => $this->getRelatedEntity()->getIdentifyingNameTranslateObject(),
]);
}
When I need to return such a translated property, I need access to my injected TranslateObjectHelper service and use its trans() method like in a controller or any other service:
$this->translateObjectHelper->trans($myObject->getIdentifyingNameTranslateObject());
Then I created a twig filter as a simple helper like this:
namespace App\Twig;
use App\Services\TranslateObjectHelper;
use App\Utils\TranslateObject;
class TranslateObjectExtension extends \Twig_Extension
{
/** #var TranslateObjectHelper */
protected $translateObjectHelper;
public function __construct(TranslateObjectHelper $translateObjectHelper)
{
$this->translateObjectHelper = $translateObjectHelper;
}
public function getFilters()
{
return array(
new \Twig_SimpleFilter('translateObject', [$this, 'translateObject']),
);
}
/**
* sends a TranslateObject through a the translateObjectHelper->trans() method
*/
public function translateObject(TranslateObject $translateObject): string
{
return $this->translateObjectHelper->trans($translateObject);
}
public function getName(): string
{
return 'translate_object_twig_extension';
}
}
So in Twig I can translate like this:
{{ myObject.getIdentifyingNameTranslateObject()|translateObject }}
In the end, I "just" needed to find all getIdentifyingName() calls (or .identifyingName in Twig) on that entities and replace them with getIdentifyingNameTranslateObject() with a call to the trans() method of the TranslateObjectHelper (or the translateObject filter in Twig).