Abstract Factory for custom Validators - options discarded? - php

My goal is to pass custom options to validators, as is done by the ZF2-provided validators. Consider this validator config:
'filters' => array(
'leaderboard' => array(
'required' => true,
'filters' => array(
array('name' => 'stringtrim'),
),
'validators' => array(
array(
'name' => '\LDP\Form\Validator\UniqueAtom',
'options' => array(
'key' => 'foo',
),
),
),
),
),
In this case, my validator is provided by an abstract factory which is specified in my application's getValidatorConfig(). It seems though, judging by lines 95+ in AbstractPluginManager that this function sequence ignores creation options:
public function get($name, $options = array(), $usePeeringServiceManagers = true)
{
// Allow specifying a class name directly; registers as an invokable class
if (!$this->has($name) && $this->autoAddInvokableClass && class_exists($name)) {
$this->setInvokableClass($name, $name);
}
$this->creationOptions = $options;
$instance = parent::get($name, $usePeeringServiceManagers);
$this->creationOptions = null;
$this->validatePlugin($instance);
return $instance;
}
In short, the creation options make their way there, but they're never ferried about. What's the best solution?

After banging my head against a wall for a very long time, I've just found the solution in the source. You have to make your factory implement Zend\ServiceManager\MutableCreationOptionsInterface
You can then use whatever it passes to instantiate the "next thing". Feels like an official band-aid hehe, but it works.
Hope this helps.

Related

How to extend Parsedown to add a class to table tags

I want to write an extension for Parsedown so that I can add a default class to each of the table tags. I've found that I can successfully hack the source code by adding lines to assign attributes in the blockTable function (around line 870):
$Block = array(
'alignments' => $alignments,
'identified' => true,
'element' => array(
'name' => 'table',
'handler' => 'elements',
'attributes' => array(
'class' => 'table',
),
),
);
However, if I try to loosely follow the Change Element Markup extension tutorial I am unsuccessful (perhaps because the table parsing may be an iterative process and the example in the tutorial is a simple string replacement.)
I've tried:
class Extension extends Parsedown
{
protected function blockTable($Line, array $Block = null)
{
$Block = parent::blockTable($Line, array $Block = null);
$Block['table']['attributes']['class'] = 'table';
return $Block;
}
}
but that doesn't work.
I'm not too sure what's wrong with your code, as your code matches mine. I simply added
'attributes' => array(
'class' => 'table table-responsive'
),
to identifyTable, on line 850 so that it became
$Block = array(
'alignments' => $alignments,
'identified' => true,
'element' => array(
'name' => 'table',
'handler' => 'elements',
'attributes' => array(
'class' => 'table table-responsive',
),
),
);
which works just fine for me. But that appears to be the same for you, minus table-responsive.
What version are you using?
I know this is a very old question asked 5 years, 3 month ago, but I came across this answer on a google search, so thought it was worthwhile answering with the code which outputs table class.
class Extension extends Parsedown {
protected function blockTable($Line, ?array $Block = null)
{
$Block = parent::blockTable($Line, $Block);
if(is_null($Block)){ return; }
$Block['element']['attributes']['class'] = 'table table-responsive';
return $Block;
}
I encountered exactly the same problem with the symfony demo application.
Finally it turned out that it wasn't parsedown, because the output was cleaned up by the html-sanitizer.
Allowing the class attribute for tables solved the issue.
For symfony 4 demo app add to config/packages/html_sanitizer.yaml:
html_sanitizer:
#[...]
sanitizers:
default:
# [...]
tags:
table:
allowed_attributes:
- "class"

ZF2 Form and Doctrine 2 modify the value_options

I am using Doctrine 2 in my Zend Framework 2 Project. I have now created a Form and create one of my Dropdowns with Values from the Database. My Problem now is that I want to change which values are used and not the one which I get back from my repository. Okay, here some Code for a better understanding:
$this->add(
array(
'type' => 'DoctrineModule\Form\Element\ObjectSelect',
'name' => 'county',
'options' => array(
'object_manager' => $this->getObjectManager(),
'label' => 'County',
'target_class' => 'Advert\Entity\Geolocation',
'property' => 'county',
'is_method' => true,
'empty_option' => '--- select county ---',
'value_options'=> function($targetEntity) {
$values = array($targetEntity->getCounty() => $targetEntity->getCounty());
return $values;
},
'find_method' => array(
'name' => 'getCounties',
),
),
'allow_empty' => true,
'required' => false,
'attributes' => array(
'id' => 'county',
'multiple' => false,
)
)
);
I want to set the value for my Select to be the County Name and not the ID. I thought that I would need the 'value_options' which needs an array. I tried it like above, but get the
Error Message: Argument 1 passed to Zend\Form\Element\Select::setValueOptions() must be of the type array, object given
Is this possible at all?
I was going to suggest modifying your code, although after checking the ObjectSelect code i'm surprised that (as far as I can tell) this isn't actually possible without extending the class. This is because the value is always generated from the id.
I create all form elements using factories (without the ObjectSelect), especially complex ones that require varied lists.
Alternative solution
First create a new method in the Repository that returns the correct array. This will allow you to reuse that same method should you need it anywhere else (not just for forms!).
class FooRepository extends Repository
{
public function getCounties()
{
// normal method unchanged, returns a collection
// of counties
}
public function getCountiesAsArrayKeyedByCountyName()
{
$counties = array();
foreach($this->getCounties() as $county) {
$counties[$county->getName()] = $county->getName();
}
return $counties;
}
}
Next create a custom select factory that will set the value options for you.
namespace MyModule\Form\Element;
use Zend\Form\Element\Select;
use Zend\ServiceManager\ServiceLocatorInterface;
use Zend\ServiceManager\FactoryInterface;
class CountiesByNameSelectFactory implements FactoryInterface
{
public function createService(ServiceLocatorInterface $formElementManager)
{
$element = new Select;
$element->setValueOptions($this->loadValueOptions($formElementManager));
// set other select options etc
$element->setName('foo')
->setOptions(array('foo' => 'bar'));
return $element;
}
protected function loadValueOptions(ServiceLocatorInterface $formElementManager)
{
$serviceManager = $formElementManager->getServiceLocator();
$repository = $serviceManager->get('DoctrineObjectManager')->getRepository('Foo/Entity/Bar');
return $repository->getCountiesAsArrayKeyedByCountyName();
}
}
Register the new element with the service manager by adding a new entry in Module.php or module.config.php.
// Module.php
public function getFormElementConfig()
{
return array(
'factories' => array(
'MyModule\Form\Element\CountiesByNameSelect'
=> 'MyModule\Form\Element\CountiesByNameSelectFactory',
),
);
}
Lastly change the form and remove your current select element and add the new one (use the name that you registered with the service manager as the type key)
$this->add(array(
'name' => 'counties',
'type' => 'MyModule\Form\Element\CountiesByNameSelect',
));
It might seem like a lot more code (because it is) however you will benefit from it being a much clearer separation of concerns and you can now reuse the element on multiple forms and only need to configure it in one place.

Zf2 Form dependent on userId

I want to create a dropdown unique to the users, and found a way to do that, but its not ok with the client.
here is what i`ve done:
public function __construct($name = null, EntityManager $em = null, $userId = null)
{
parent::__construct($name);
$this->setAttribute('method', 'post');
[...]
$this->add(array(
'name' => 'religionId',
'type' => 'DoctrineModule\Form\Element\ObjectSelect',
'options' => array(
'object_manager' => $this->getEntityManager(),
'target_class' => 'Religions\Entity\Religions',
'property' => 'name',
'disable_inarray_validator' => true,
'by_reference' => false,
'is_method' => true,
'find_method' => array(
'name' => 'findBy',
'params' => array(
'criteria' => array('reUserId' => $userId),
'orderBy' => array('name' => 'ASC'),
),
),
),
'attributes' => array(
'multiple' => false,
'required' => false,
)
));
}
This worked, as i was sending the variable reuserId when initializing the form, using $this->identity()
The client wants to inject the user entity and select from there....
Searched stackoverflow and google, but was not able to find anything...any help please? thanks!
To 'inject' anything in ZF2 you will need to use the ServiceManager and create service factories.
It is therefore important to ensure that you are always creating the form via the ServiceManager.
In a controller for instance:
$form = $this->getServiceLocator()->get('MyModule\Form\FooForm');
Then you would need to 'inject' the user buy creating a factory class or closure.
Module.php
public function getFormElementConfig() {
return array(
'factories' => array(
'MyModule\Form\FooForm' => function($fem) {
$serviceManager = $fem->getServiceLocator();
$entityManager = $serviceManager->get('objectmanager'); // Doctrine object manager
// Load the user
// Example is the zfcUser authentication service, however replace
// this with whatever you use to maintain the users id
$user = $serviceManager->get('zfcuser_auth_service')->getIdentity();
// Inject the user entity into the form constructor
$form = new FooForm($user);
return $form;
},
),
);
}
With that said I think you might need to think about the form dependencies. It seems to me that you do not depend on the user entity - but rather the user's id should be used in a database query that reduces the list of 'Religions'.
You could execute this query and then pass the result (The religion collection) to the form in the same way my example shows how to for the user entity - This would then mean you could use a 'normal' Zend\Form\Element\Select rather than the ObjectSelect - meaning no need to inject the ObjectManager etc.

zf2 ServiceManagerAwareInterface in Fieldset

In my Zend\Form\Fieldset AddressFieldset it needs a Zend\Db\TableGateway\AbstractTableGateway BundeslandTable for a \Zend\Form\Element\Select().
So i implement \Zend\ServiceManager\ServiceManagerAwareInterface in this AddressFieldset and use the init() instead __construct().
And in module.config.php (not only in 'form_elements' tested, also in 'service_manager')
'form_elements' => array(
'factories' => array(
'MyFormway\Form\Fieldset\Address' => function($sm) {
$addressFieldset = new MyFormway\Form\Fieldset\AddressFieldset();
$addressFieldset->setServiceManager($sm);
return $addressFieldset;
}
),
),
In a \Zend\Form\Form's init():
$this->add(array(
'type' => 'MyFormway\Form\Fieldset\Address',
'name' => 'address',
));
this throws an error:
Zend\Form\FormElementManager::get was unable to fetch or create an instance for MyFormway\Form\Fieldset\Address
Why is zend unable to fetch an instance of this Fieldset?
edit-----------------------
'form_elements' => array(
'factories' => array(
'MyFormway\Form\Fieldset\Address' => function($formElementManager) {
die('inna form_elements config');
$addressFieldset = new \MyFormway\Form\Fieldset\AddressFieldset();
$addressFieldset->setServiceManager($formElementManager->getServiceLocator());
return $addressFieldset;
}
),
),
Because i have the Zend\Form\FormElementManager i fetch the ServiceLocator ...perhaps dont needed, because all XxxManager extends the Zend\ServiceManager\AbstractPluginManager and this extends ServiceManager.
In FormElementManager and also in AbstractPluginManager are no method getServiceManager().
But my problem: the die() is not called plus the error above. Is it a bug? ...i stand for a big wall :(
edit-----------------------
It works for a Form but not for a Fieldset!!!
Can you do a quick check if the \Invokable is called at all? Some professional die()-debugging will suffice.
Other than that a potential error source would be your injection of the ServiceManager. In the code you provide you're not actually injecting the ServiceLocator but rather the FormElementManager.
$addressFieldset->setServiceManager($sm->getServiceManager());
Doing it this way is considered Bad-Practice tho. You should only inject the stuff that you actually do need. Given you're injecting the whole manager i assume you're either working with Doctrine or you'll need access to some DB-Data. Do it like this:
'Foo' => function ($formElementManager) {
$sl = $formElementManager->getServiceManager();
$fs = new FooFieldset();
$fs->setDbDependency(
$sl->get('MyDbDependency')
);
return $fs;
}
Last little note: when you're adding a Fieldset, you don't need to add 'name' => 'foo' within the $this->add(), since the name of the fieldset will be defined via the Fieldset __construct('name').

How to inject the Doctrine ObjectManager into form element

I'm working on my custom User module, which basically uses ZfcUser as the foundation. Since every application is different and requires different meta information about users I want this to be easily configurable using a config array.
In my module's global config file I define the custom form fields and in the Module's onBootstrap I extend the ZfcUser registration form using the init event of ZfcUser\Form\Register. So in short I want to do something like this:
$sharedEvents->attach('ZfcUser\Form\Register',
'init',
function($e) use ($sm)
{
/* #var $form \ZfcUser\Form\Register */
$form = $e->getTarget();
// Get relevant config
$config = $sm->get('config');
if ( array_key_exists('redev_user', $config) && is_array($config['redev_user']) )
{
if ( array_key_exists('custom_fields', $config['redev_user']) && is_array($config['redev_user']['custom_fields']) )
{
foreach ($config['redev_user']['custom_fields'] as $curCustomField)
{
$form->add($curCustomField);
}
}
}
[...]
In my config file I then define the custom form fields like this:
<?php
return array(
'redev_user' => array(
'custom_fields' => array(
// Custom fields which will be added to the registration form
array(
'name' => 'firstname',
'type' => 'text',
'options' => array(
'label' => 'First name',
),
),
I do the same thing for the validators; they are being defined in the config file and attached to the form elements in the onBootstrap.
This all works nice and dandy, except when I need a Doctrine form element. In my specific case I would like to use a DoctrineModule\Form\Element\ObjectSelect for the country selectbox. In my config this would look like this:
array(
'name' => 'country',
'type' => 'DoctrineModule\Form\Element\ObjectSelect',
'options' => array(
'label' => 'Country',
//'object_manager' => $sm->get('Doctrine\ORM\EntityManager'),
'target_class' => 'RedevUser\Entity\Country',
'property' => 'countryname',
'is_method' => false,
'find_method' => array(
'name' => 'findBy',
'params' => array(
'criteria' => array(),
'orderBy' => array('countryname' => 'ASC'),
),
),
),
),
Note the commented out line for the object_manager. The ObjectSelect element obviously needs the ObjectManager. The question is how do I inject the ObjectManager while rendering the form based on the config.
I was thinking to render the form element myself and then check if it's an instance of some interface or base class of DoctrineModule\Form\Element. However it turns out there is no such base class or interface. The only thing those elements have in common is that they have a getProxy. Right now my code in onBootstrap looks like this:
foreach ($config['redev_user']['custom_fields'] as $curCustomField)
{
$formElemFactory = $form->getFormFactory();
$elem = $formElemFactory->createElement($curCustomField);
if ($elem instanceof \DoctrineModule\Form\Element\ObjectSelect)
{
// Inject ObjectManager
$elem->getProxy()->setObjectmanager($sm->get('Doctrine\ORM\EntityManager'));
}
$form->add($elem);
}
But I don't really want to check for the different Doctrine form element types. Also it seems a bit dirty to do it like this. Any opinions or ideas of how to do this better/cleaner?

Categories