Dynamically instantiating a class based on string in PHP - php

Im currently getting my hands dirty with some subclassing object oriented php. I would like to use an array to create some form fields, and these fields are separated into classes based on their type. This means that I have a main class called "form_field", and then have a bunch of subclasses called "form_field_type" (ex. "form_field_select"). The idea is that each subclass "knows" how to best generate their HTML in a display method.
So lets say that i write an array like this:
$fields = array(
array(
'name' => 'field1',
'type' => 'text',
'label' => 'label1',
'description' => 'desc1',
'required' => true,
),
array(
'name' => 'field2',
'type' => 'select',
'label' => 'label1',
'description' => 'desc1',
'options' => array(
'option1' => 'Cat',
'option2' => 'Dog',
),
'ui' => 'select2',
'allow_null' => false,
)
);
I would then like to create a loop that instantiates the correct class based on the type:
foreach ($fields as $field) {
$type = $field['type'];
$new_field = // instantiate the correct field class here based on type
$new_field->display();
}
What would be the best approach here? I would like to avoid doing something like:
if ($type == 'text') {
$new_field = new form_field_text();
} else if ($type == 'select') {
$new_field = new form_field_select();
} // etc...
This just feels inefficient, and i feel like there must be a better way? Is there a good pattern that is generally used in this situation, or am I going about solving this the wrong way?

Try something like this...
foreach ($fields as $field) {
$type = $field['type'];
// instantiate the correct field class here based on type
$classname = 'form_field_' .$type;
if (!class_exists($classname)) { //continue or throw new Exception }
// functional
$new_field = new $classname();
// object oriented
$class = new ReflectionClass($classname);
$new_field = $class->newInstance();
$new_field->display();
}

Related

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.

Abstract Factory for custom Validators - options discarded?

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.

Parse date in ZF2

I'm having a problem.
I'm using ZF2, and getting an error here.
$inputFilter->add($factory->createInput(array(
'name' => 'dataInicial',
'required' => true,
'validators' => array(
array(
'name' => 'NotEmpty',
'options' => array(
'messages' => array(
\Zend\Validator\NotEmpty::IS_EMPTY => 'A data do feriado deve ser preenchida.'
)
)
),
array(
'name' => 'callback',
'options' => array(
'messages' => array(
\Zend\Validator\Callback::INVALID_CALLBACK => 'Data no formato inválido.'
),
'callback' => function ($value, $context = array()) {
$dataInicial = \DateTime::createFromFormat('d/m/Y', $value);
return $dataInicial->format('d/m/Y');
}
)
)
)
)));
I'm getting this error:
DateTime::__construct(): Failed to parse time string (25/12/2014) at position 0 (2): Unexpected character
When I execute the same code in pure php, without zend, it works ok.
echo DateTime::createFromFormat('d/m/Y', '25/12/2014')->format('d/m/Y');
Maybe someone knows what's causing the error? Sorry for my english
Don't know what kind of database backend and relationship mapping but the issue is related to how the date is being stored I imagine, most likely like: Y/m/d
If you saving and getting this error then it's because you need the date in this format. If you are hydrating you may also need a date strategy to deal with hydration like:
// InputFilter.php
class yourInputFilter()
{
$hydrator->getHydrator()->addStrategy('my_attribute', new MyDateHydrationStrategy());
$this->add( array(
'name' => 'registration_starts',
'type' => 'Zend\Form\Element\DateTime',
'options' => array (
'format'=>'Y/m/d',
)
));
}
// YourForm.php
class YourForm Extends ....
{
$dateTime = new Element\DateTime('youDate');
$dateTime->setLabel('Date')
->setOptions(array(
'format' => 'Y-m-d\TH:iP'
));
}
// Strategy.php
class myDateTimeStrategy implements StrategyInterface
{
public function hydrate($value)
{
if (is_string($value)) {
$value = new DateTime($value);
}
// Could return in a different format, probably can't though because of when you need to edit
// return $value->format("m-d-Y")
return $value;
}
}
You will also need to change js settings if you using a datetime widget picker:
Which will be something like:
$('.datepicker').datepicker('format', 'yyyy/mm/dd');
Depending on which one you using.

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?

Zend Framework Form Irrational Behaviour

Let's start this off with a short code snippet I will use to demonstrate my opinion:
$title = new Zend_Form_Element_Text('title', array(
'label' => 'Title',
'required' => false,
'filters' => array(
'StringTrim',
'HtmlEntities'
),
'validators' => array(
array('StringLength', false, array(3, 100))
),
));
This important line is:
'required' => false,
Which means that the input field is not required and you can submit the form without filling it. However, this also means that any filters and validators won't apply to it if you choose to fill in this field.
Common sense tells me that is an irrational behavior. The way I understand the word 'required' in relation with HTML input fields: an input field that is not required should return NULL if it is not filled in but if user decides to fill it both filters and validators should apply to it. That's what makes sense to me. Do you agree with me or is my common sense not so common?
Now more practical question, because this is how Zend_Form behaves, how can I achieve not required fields which would work as I described above (if nothing is typed in by user it returns NULL otherwise filters and validators normally apply).
Not really a complete answer to your question, but since comments don't have syntax formatting; here's a filter you can use to make your field values null if empty.
class My_Filter_NullIfEmpty implements Zend_Filter_Interface
{
public function filter( $value )
{
// maybe you need to expand the conditions here
if( 0 == strlen( $value ) )
{
return null;
}
return $value;
}
}
About the required part:
I'm not sure really. You could try to search the ZF mailinglists on Nabble:
http://www.nabble.com/Zend-Framework-Community-f16154.html
Or subscribe to their mailinglist, and ask them the question. Either through Nabble, or directly via the addresses on framework.zend.com:
http://tinyurl.com/y4f9lz
Edit:
Ok, so now I've done some tests myself, cause what you said all sounded counter intuitive to me. Your example works fine with me. This is what I've used:
<?php
class Form extends Zend_Form
{
public function init()
{
$title = new Zend_Form_Element_Text('title', array(
'label' => 'Title',
'required' => false,
'filters' => array(
'StringTrim',
'HtmlEntities',
'NullIfEmpty' // be sure this one is available
),
'validators' => array(
array('StringLength', false, array(3, 100))
),
));
$this->addElement( $title );
}
}
$form = new Form();
$postValues = array( 'title' => '' ); // or
$postValues = array( 'title' => ' ' ); // or
$postValues = array( 'title' => 'ab' ); // or
$postValues = array( 'title' => ' ab ' ); // or
$postValues = array( 'title' => '<abc>' ); // all work perfectly fine with me
// validate the form (which automatically sets the values in the form object)
if( $form->isValid( $postValues ) )
{
// retrieve the relevant value
var_dump( $form->getValue( 'title' ) );
}
else
{
echo 'form invalid';
}
?>
Actually, what you describe as your expectations are exactly how Zend_Form works. If you mark the element as not required, then the following happens: (a) if no value is passed, it skips validation, but if (b) a value is passed, then it must pass all validators in order to be valid.
BTW, best place to ask ZF questions is on the ZF mailing lists: http://framework.zend.com/archives

Categories