Set default data on Symfony text field with data transformer - php

I am working on a project to dynamically create Symfony forms on the fly using a yaml file (or any other data source that may return form description as an associative array). To handle things better I want to wrap Symfony's core types to work with a data object instead of scalar data. The data object will be a simple wrapper with getter-setter.
class SimpleFormData implements FormData {
/**
* #var mixed data
*/
private $data;
/**
* #param $data
*/
function __construct($data) {
$this->data = $data;
}
/**
* #return mixed
*/
public function getData() {
return $this->data;
}
/**
* #param mixed $data
*/
public function setData($data) {
$this->data = $data;
}
}
I started with writing a data transformer:
class SimpleFormDataTransformer implements DataTransformerInterface {
/**
* #param mixed $value The value in the original representation
* #return mixed The value in the transformed representation
* #throws TransformationFailedException When the transformation fails.
*/
public function transform($value) {
if(is_null($value)) {
return '';
}
if($value instanceof FormData) {
return $value->getData();
}
$actualType = is_object($value) ? 'an instance of class '.get_class($value) : ' a(n) '.gettype($value);
$message = sprintf("Expected argument of type MyApp\\FormBundle\\FormData\\FormData, got %s", $actualType);
throw new TransformationFailedException($message);
}
/**
* #param mixed $value The value in the transformed representation
* #return mixed The value in the original representation
* #throws TransformationFailedException When the transformation fails.
*/
public function reverseTransform($value) {
return new SimpleFormData($value);
}
}
I created a custom TextType and set model transformer to it:
class TextType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options) {
$transformer = new SimpleFormDataTransformer();
$builder->addModelTransformer($transformer);
}
/**
* Returns the name of this type.
*
* #return string The name of this type
*/
public function getName() {
return 'form_bundle_type_core_text';
}
public function getParent() {
return 'text';
}
}
Everything works fine if I create a TextType and set data by calling setData method:
$form = $this->createForm(new TextType());
$form->setData(new SimpleFormData("test"));
If I try passing default data, I get an error:
$form = $this->createForm(new TextType(), new SimpleFormData("test"));
The error thrown says:
The form's view data is expected to be an instance of class
MyApp\FormBundle\Model\SimpleFormData, but is a(n) string. You can
avoid this error by setting the "data_class" option to null or by
adding a view transformer that transforms a(n) string to an instance
of MyApp\FormBundle\Model\SimpleFormData.
I certainly don't want viewData to be of type SimpleFormData (hence the transformer). Also, I haven't set data_class, Symfony's Form class picks data class from the passed data object.
I have two questions:
What should I do to be able to pass default data?
Why Symfony expects viewData to be of type data_class? My understanding was that modelData should be of type data_class and not viewData.

Related

Doctrine Data Type (use Factory)

i want to implement a new doctrine data type (http://docs.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/types.html).
I have implemented an Country Service which loads country data via adapter from any library. Know i have the following implementation:
<?php
interface CountryInterface;
interface Address
{
public function setCountry(CountryInterface $country);
public function getCountry() : CountryInterface;
}
?>
So, what I want to do is - make a CountryType which converts the Country Object to an specific string value (used field will be set via OptionClass, ex.: Alpha2, Alpha3, IsoNumber).
My problem is, doctrine only allows data types mapping via classname, so I can't implement an factory to load all needed dependencies.
I hope this is understandable.
regards
First you will need to register your custom DBAL type for country extending the Doctrine\DBAL\Types\Type class:
<?php
namespace Application\DBAL\Types;
use Application\Resource\Country;
use Application\Service\CountryService;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Types\ConversionException;
use Doctrine\DBAL\Types\Type;
use InvalidArgumentException;
class CountryType extends Type
{
const NAME = 'country';
/**
* Country service
*/
protected $countryService;
/**
* #return string
*/
public function getName()
{
return self::NAME;
}
/**
* {#inheritdoc}
*/
public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform)
{
return $platform->getDoctrineTypeMapping('text');
}
/**
* {#inheritdoc}
*/
public function convertToDatabaseValue($value, AbstractPlatform $platform)
{
if($value === null){
return null;
}
if ($value instanceof Country) {
return (string) $value;
}
throw ConversionException::conversionFailed($value, self::NAME);
}
/**
* {#inheritdoc}
*/
public function convertToPHPValue($value, AbstractPlatform $platform)
{
if($value === null){
return null;
}
$country = $this->countryService->getCountry($value);
if( ! $country instanceof Country ){
throw ConversionException::conversionFailed($value, self::NAME);
}
return $country;
}
/**
* Set country service
*
* #var CountryService $service
*/
public function setCountryService ($service){
$this->countryService = $service;
}
}
This type needs to implement four methods getName, getSQLDeclaration, convertToDatabaseValue and convertToPHPValue.
Thre first one returns the name for the type
The second one is for the type declaration in the (SQL) database (I used text in the example, but you can also use integer or any other valid doctrine database type).
The third method converts your country object to a database value (so in this case a text value).
The last method does the opposite; it converts the text value from the database. In this case I just instantiate the Country class and pass the database value to the constructor. You need to add your custom logic inside your class constructor.
In my example I assume that null values are also allowed.
A simple version of your Country class could look like this:
<?php
namespace Application\Resource;
class Country{
protected $value;
/**
* Magic stringify to cast country object to a string
*/
public function __toString(){
return $value;
}
/**
* Constructor method
*/
public function construct($value){
$this->value = $value
// set other properties...
}
// setters and getters...
}
It is up to you whether value should be alpha2/alpha3/country_name or whatever you want visible in the database. You should somehow also populate the other country with the other properties in the constructor method. I leave this part up to you.
Now you need to register your custom country type so doctrine will use it:
'doctrine' => array(
//...
'configuration' => array(
'orm_default' => array(
Application\DBAL\Types\CountryType::NAME => Application\DBAL\Types\CountryType::class,
)
)
)
And you can set your service on bootstrap in your application Module.php file:
/**
* #param EventInterface|MvcEvent $event
* #return void
*/
public function onBootstrap(EventInterface $event)
{
$application = $event->getApplication();
$eventManager = $application->getEventManager();
$eventManager->attach(MvcEvent::EVENT_BOOTSTRAP, array($this, 'initializeCountryType');
}
/**
* #param MvcEvent $event
*/
public function initializeCountryType(MvcEvent $event)
{
$application = $event->getApplication();
$serviceManager = $application->getServiceManager();
//get your country service from service manager
$countryService = $serviceManager->getCountryService();
$countryType = \Doctrine\DBAL\Types\Type::getType('country');
$countryType->setCountryService($countryService);
}
Now you can use your country type in any entity definition as follows:
/**
* #var string
* #ORM\Column(type="country", nullable=true)
*/
protected $country;
Read more on how to map custom DBAL types in the Doctrine2 documentation chapter Custom Mapping Types

Can symfony serializer deserialize return nested entity of type child entity?

When I deserialize my doctrine entity, the initial object is constructed/initiated correctly, however all child relations are trying to be called as arrays.
The root level object's addChild(ChildEntity $entity) method is being called, but Symfony is throwing an error that addChild is receiving an array and not an instance of ChildEntity.
Does Symfony's own serializer have a way to deserialize nested arrays (child entities) to the entity type?
JMS Serializer handles this by specifying a #Type("ArrayCollection<ChildEntity>") annotation on the property.
I believe the Symfony serializer attempts to be minimal compared to the JMS Serializer, so you might have to implement your own denormalizer for the class. You can see how the section on adding normalizers.
There may be an easier way, but so far with Symfony I am using Discriminator interface annotation and type property for array of Objects. It can also handle multiple types in one array (MongoDB):
namespace App\Model;
use Symfony\Component\Serializer\Annotation\DiscriminatorMap;
/**
* #DiscriminatorMap(typeProperty="type", mapping={
* "text"="App\Model\BlogContentTextModel",
* "code"="App\Model\BlogContentCodeModel"
* })
*/
interface BlogContentInterface
{
/**
* #return string
*/
public function getType(): string;
}
and parent object will need to define property as interface and get, add, remove methods:
/**
* #var BlogContentInterface[]
*/
protected $contents = [];
/**
* #return BlogContentInterface[]
*/
public function getContents(): array
{
return $this->contents;
}
/**
* #param BlogContentInterface[] $contents
*/
public function setContents($contents): void
{
$this->contents = $contents;
}
/**
* #param BlogContentInterface $content
*/
public function addContent(BlogContentInterface $content): void
{
$this->contents[] = $content;
}
/**
* #param BlogContentInterface $content
*/
public function removeContent(BlogContentInterface $content): void
{
$index = array_search($content, $this->contents);
if ($index !== false) {
unset($this->contents[$index]);
}
}

symfony : can't we have a hidden entity field?

I am rendering a form with an entity field in symfony.
It works well when i choose a regular entity field.
$builder
->add('parent','entity',array(
'class' => 'AppBundle:FoodAnalytics\Recipe',
'attr' => array(
'class' => 'hidden'
)
))
It throws the following error when I choose ->add('parent','hidden') :
The form's view data is expected to be of type scalar, array or an
instance of \ArrayAccess, but is an instance of class
AppBundle\Entity\FoodAnalytics\Recipe. You can avoid this error by
setting the "data_class" option to
"AppBundle\Entity\FoodAnalytics\Recipe" or by adding a view
transformer that transforms an instance of class
AppBundle\Entity\FoodAnalytics\Recipe to scalar, array or an instance
of \ArrayAccess. 500 Internal Server Error - LogicException
Can't we have hidden entity fields ?? Why not? Am I obliged to put another hidden field to retrieve the entity id?
EDIT :
Basically, what I'm trying to do is to hydrate the form before displaying it but prevent the user to change one of its fields (the parent here).
This is because I need to pass the Id as a parameter and I can't do it in the form action url.
I think you are simply confused about the field types and what they each represent.
An entity field is a type of choice field. Choice fields are meant to contain values selectable by a user in a form. When this form is rendered, Symfony will generate a list of possible choices based on the underlying class of the entity field, and the value of each choice in the list is the id of the respective entity. Once the form is submitted, Symfony will hydrate an object for you representing the selected entity. The entity field is typically used for rendering entity associations (like for example a list of roles you can select to assign to a user).
If you are simply trying to create a placeholder for an ID field of an entity, then you would use the hidden input. But this only works if the form class you are creating represents an entity (ie the form's data_class refers to an entity you have defined). The ID field will then properly map to the ID of an entity of the type defined by the form's data_class.
EDIT: One solution to your particular situation described below would be to create a new field type (let's call it EntityHidden) that extends the hidden field type but handles data transformation for converting to/from an entity/id. In this way, your form will contain the entity ID as a hidden field, but the application will have access to the entity itself once the form is submitted. The conversion, of course, is performed by the data transformer.
Here is an example of such an implementation, for posterity:
namespace My\Bundle\Form\Extension\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\DataTransformerInterface;
/**
* Entity hidden custom type class definition
*/
class EntityHiddenType extends AbstractType
{
/**
* #var DataTransformerInterface $transformer
*/
private $transformer;
/**
* Constructor
*
* #param DataTransformerInterface $transformer
*/
public function __construct(DataTransformerInterface $transformer)
{
$this->transformer = $transformer;
}
/**
* #inheritDoc
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
// attach the specified model transformer for this entity list field
// this will convert data between object and string formats
$builder->addModelTransformer($this->transformer);
}
/**
* #inheritDoc
*/
public function getParent()
{
return 'hidden';
}
/**
* #inheritDoc
*/
public function getName()
{
return 'entityhidden';
}
}
Just note that in the form type class, all you have to do is assign your hidden entity to its corresponding form field property (within the form model/data class) and Symfony will generate the hidden input HTML properly with the ID of the entity as its value. Hope that helps.
Just made this on Symfony 3 and realised it's a bit different from what has already been posted here so I figured it was worth sharing.
I just made a generic data transformer that could be easily reusable across all your form types. You just have to pass in your form type and that's it. No need to create a custom form type.
First of all let's take a look at the data transformer:
<?php
namespace AppBundle\Form;
use Doctrine\Common\Persistence\ObjectManager;
use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\Exception\TransformationFailedException;
/**
* Class EntityHiddenTransformer
*
* #package AppBundle\Form
* #author Francesco Casula <fra.casula#gmail.com>
*/
class EntityHiddenTransformer implements DataTransformerInterface
{
/**
* #var ObjectManager
*/
private $objectManager;
/**
* #var string
*/
private $className;
/**
* #var string
*/
private $primaryKey;
/**
* EntityHiddenType constructor.
*
* #param ObjectManager $objectManager
* #param string $className
* #param string $primaryKey
*/
public function __construct(ObjectManager $objectManager, $className, $primaryKey)
{
$this->objectManager = $objectManager;
$this->className = $className;
$this->primaryKey = $primaryKey;
}
/**
* #return ObjectManager
*/
public function getObjectManager()
{
return $this->objectManager;
}
/**
* #return string
*/
public function getClassName()
{
return $this->className;
}
/**
* #return string
*/
public function getPrimaryKey()
{
return $this->primaryKey;
}
/**
* Transforms an object (entity) to a string (number).
*
* #param object|null $entity
*
* #return string
*/
public function transform($entity)
{
if (null === $entity) {
return '';
}
$method = 'get' . ucfirst($this->getPrimaryKey());
// Probably worth throwing an exception if the method doesn't exist
// Note: you can always use reflection to get the PK even though there's no public getter for it
return $entity->$method();
}
/**
* Transforms a string (number) to an object (entity).
*
* #param string $identifier
*
* #return object|null
* #throws TransformationFailedException if object (entity) is not found.
*/
public function reverseTransform($identifier)
{
if (!$identifier) {
return null;
}
$entity = $this->getObjectManager()
->getRepository($this->getClassName())
->find($identifier);
if (null === $entity) {
// causes a validation error
// this message is not shown to the user
// see the invalid_message option
throw new TransformationFailedException(sprintf(
'An entity with ID "%s" does not exist!',
$identifier
));
}
return $entity;
}
}
So the idea is that you call it by passing the object manager there, the entity that you want to use and then the field name to get the entity ID.
Basically like this:
new EntityHiddenTransformer(
$this->getObjectManager(),
Article::class, // in your case this would be FoodAnalytics\Recipe::class
'articleId' // I guess this for you would be recipeId?
)
Let's put it all together now. We just need the form type and a bit of YAML configuration and then we're good to go.
<?php
namespace AppBundle\Form;
use AppBundle\Entity\Article;
use Doctrine\Common\Persistence\ObjectManager;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* Class JustAFormType
*
* #package AppBundle\CmsBundle\Form
* #author Francesco Casula <fra.casula#gmail.com>
*/
class JustAFormType extends AbstractType
{
/**
* #var ObjectManager
*/
private $objectManager;
/**
* JustAFormType constructor.
*
* #param ObjectManager $objectManager
*/
public function __construct(ObjectManager $objectManager)
{
$this->objectManager = $objectManager;
}
/**
* #return ObjectManager
*/
public function getObjectManager()
{
return $this->objectManager;
}
/**
* {#inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('article', HiddenType::class)
->add('save', SubmitType::class);
$builder
->get('article')
->addModelTransformer(new EntityHiddenTransformer(
$this->getObjectManager(),
Article::class,
'articleId'
));
}
/**
* {#inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => 'AppBundle\Entity\MyEntity',
]);
}
}
And then in your services.yml file:
app.form.type.article:
class: AppBundle\Form\JustAFormType
arguments: ["#doctrine.orm.entity_manager"]
tags:
- { name: form.type }
And in your controller:
$form = $this->createForm(JustAFormType::class, new MyEntity());
$form->handleRequest($request);
That's it :-)
With Symfony 5, I use the solution of a Hidden type that implements DataTransformerInterface interface.
<?php
namespace App\Form\Type;
use Doctrine\Persistence\ManagerRegistry;
use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\Exception\TransformationFailedException;
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
use Symfony\Component\Form\FormBuilderInterface;
/**
* Defines the custom form field type used to add a hidden entity
*
* See https://symfony.com/doc/current/form/create_custom_field_type.html
*/
class EntityHiddenType extends HiddenType implements DataTransformerInterface
{
/** #var ManagerRegistry $dm */
private $dm;
/** #var string $entityClass */
private $entityClass;
/**
*
* #param ManagerRegistry $doctrine
*/
public function __construct(ManagerRegistry $doctrine)
{
$this->dm = $doctrine;
}
/**
*
* {#inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options): void
{
// Set class, eg: App\Entity\RuleSet
$this->entityClass = sprintf('App\Entity\%s', ucfirst($builder->getName()));
$builder->addModelTransformer($this);
}
public function transform($data): string
{
// Modified from comments to use instanceof so that base classes or interfaces can be specified
if (null === $data || !$data instanceof $this->entityClass) {
return '';
}
$res = $data->getId();
return $res;
}
public function reverseTransform($data)
{
if (!$data) {
return null;
}
$res = null;
try {
$rep = $this->dm->getRepository($this->entityClass);
$res = $rep->findOneBy(array(
"id" => $data
));
}
catch (\Exception $e) {
throw new TransformationFailedException($e->getMessage());
}
if ($res === null) {
throw new TransformationFailedException(sprintf('A %s with id "%s" does not exist!', $this->entityClass, $data));
}
return $res;
}
}
And to use the field in the form:
use App\Form\Type\EntityHiddenType;
public function buildForm(FormBuilderInterface $builder, array $options): void
{
// Field name must match entity class, eg 'ruleSet' for App\Entity\RuleSet
$builder->add('ruleSet', EntityHiddenType::class);
}
This can be achieved fairly cleanly with form theming, using the standard hidden field theme in place of that for the entity. I think using transformers is probably overkill, given that hidden and select fields will give the same format.
{% block _recipe_parent_widget %}
{%- set type = 'hidden' -%}
{{ block('form_widget_simple') }}
{% endblock %}
A quick solution whitout creating new transformer and type classes. When you want to prepopulate an related entity from the db.
// Hidden selected single group
$builder->add('idGroup', 'entity', array(
'label' => false,
'class' => 'MyVendorCoreBundle:Group',
'query_builder' => function (EntityRepository $er) {
$qb = $er->createQueryBuilder('c');
return $qb->where($qb->expr()->eq('c.groupid', $this->groupId()));
},
'attr' => array(
'class' => 'hidden'
)
));
This results a single hidden selection like:
<select id="mytool_idGroup" name="mytool[idGroup]" class="hidden">
<option value="1">MyGroup</option>
</select>
But yes, i agree that with a little more effort by using a DataTransformer you can achieve something like:
<input type="hidden" value="1" id="mytool_idGroup" name="mytool[idGroup]"/>
That will do what you need:
$builder->add('parent', 'hidden', array('property_path' => 'parent.id'));
one advantage of using transformer with text/hidden field over entity type field is that the entity field preloads choices.
<?php declare(strict_types=1);
namespace App\Form\DataTransformer;
use Doctrine\Persistence\ObjectRepository;
use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\Exception\TransformationFailedException;
class EntityIdTransformer implements DataTransformerInterface
{
private \Closure $getter;
private \Closure $loader;
public function __construct(
private readonly ObjectRepository $repository,
\Closure|string $getter,
\Closure|string $loader)
{
$this->getter = is_string($getter) ? fn($e) => $e->{'get'.ucfirst($getter)}() : $getter;
$this->loader = is_string($loader) ? fn($id) => $this->repository->findOneBy([$loader => $id]) : $loader;
}
/**
* Transforms an object (entity) to a string|number.
*
* #param object|null $entity
* #return string|int|null
*/
public function transform(mixed $entity): string|int|null
{
if (empty($entity)) {
return null;
}
return $this->getIdentifier($entity);
}
/**
* Transforms a string|number to an object (entity).
*
* #throws TransformationFailedException if entity is not found
* #param string|int $identifier
* #return object|null
*/
public function reverseTransform(mixed $identifier): object|null
{
if (empty($identifier)) {
return null;
}
//TODO: is this needed?
if (is_object($identifier)) {
$identifier = $this->transform($identifier);
}
$entity = $this->fetchEntity($identifier);
if (null === $entity) {
throw new TransformationFailedException(sprintf(
'An entity with ID "%s" does not exist!',
$identifier
));
}
return $entity;
}
protected function getIdentifier(object $entity): int|string
{
$getter = $this->getter;
return $getter($entity);
}
protected function fetchEntity(int|string $identifier): object|null
{
$loader = $this->loader;
return $loader($identifier, $this->repository);
}
}
can be used like
$builder
->add('parent', FormType\TextType::class, [
'label' => 'Parent',
])->get('parent')->addModelTransformer(new EntityIdTransformer(
repository: $this->em->getRepository($options['data_class']),
getter: 'id',
loader: 'id',
));
or
$builder
->add('parent', FormType\TextType::class, [
'label' => 'Parent',
])->get('parent')->addModelTransformer(new EntityIdTransformer(
repository: $this->em->getRepository($options['data_class']),
getter: fn($e) => $e->getHash(),
loader: fn($id, $repo) => $repo->findOneBy(['hash' => $id])
));
or even
$builder
->add('parent', FormType\TextType::class, [
'label' => 'Parent',
])->get('parent')->addModelTransformer(new EntityIdTransformer(
repository: $this->em->getRepository($options['data_class']),
getter: 'hash',
loader: fn($id, $repo) => $repo->createQueryBuilder('e')->andWhere('e.hash = :hash')->andWhere('e.disabled = 0')->setParameter('hash', $id)->getQuery()->getOneOrNullResult()
));

How to avoid validation of passed argument inside a method? Proper OOD

Writing group of parsers that rely on one abstract class which implements shared methods and asks to implement addition method which contains per parser logic.
Abstract parser code:
<?
abstract class AbstractParser {
/*
* The only abstract method to implement. It contains unique logic of each feed passed to the function
*/
public abstract function parse($xmlObject);
/**
* #param $feed string
* #return SimpleXMLElement
* #throws Exception
*/
public function getFeedXml($feed) {
$xml = simplexml_load_file($feed);
return $xml;
}
/**
* #return array
*/
public function getParsedData() {
return $this->data;
}
/**
* #param SimpleXMLElement
* #return Array
*/
public function getAttributes($object) {
// implementation here
}
}
Concrete Parser class:
<?php
class FormulaDrivers extends AbstractParser {
private $data;
/**
* #param SimpleXMLElement object
* #return void
*/
public function parse($xmlObject) {
if (!$xmlObject) {
throw new \Exception('Unable to load remote XML feed');
}
foreach($xmlObject->drivers as $driver) {
$driverDetails = $this->getAttributes($driver);
var_dump($driver);
}
}
}
Instantiation:
$parser = new FormulaDrivers();
$parser->parse( $parser->getFeedXml('http://api.xmlfeeds.com/formula_drivers.xml') );
As you can see, I pass the result of getFeedXml method to parse method, basically delegating the validation of result of getFeedXml to parse method.
How can I avoid it, make sure it returns correct XML object before I pass it to parse method?
Increasing instantiation process and amount of called methods leads to the need of some factory method...
Anyway, how would you fix this small issue?
Thanks!
Make parse protected, so that only parse_xml_file calls it:
abstract class AbstractParser {
/*
* The only abstract method to implement. It contains unique logic of each feed passed to the function
*/
protected abstract function parse($xmlObject);
/**
* #param $feed string
* #return [whatever .parse returns]
* #throws Exception
*/
public function parseFile($feed) {
$xml = simplexml_load_file($feed);
if (!$xml) {
throw new \Exception('Unable to load remote XML feed');
}
return $this->parse($xml);
}
/**
* #return array
*/
public function getParsedData() {
return $this->data;
}
/**
* #param SimpleXMLElement
* #return Array
*/
public function getAttributes($object) {
// implementation here
}
}
$parser->parseFile('http://api.xmlfeeds.com/formula_drivers.xml');

Validation in Zend Framework 2 with Doctrine 2

I am right now getting myself more and more familiar with Zend Framework 2 and in the meantime I was getting myself updated with the validation part in Zend Framework 2. I have seen few examples how to validate the data from the database using Zend Db adapter, for example the code from the Zend Framework 2 official website:
//Check that the username is not present in the database
$validator = new Zend\Validator\Db\NoRecordExists(
array(
'table' => 'users',
'field' => 'username'
)
);
if ($validator->isValid($username)) {
// username appears to be valid
} else {
// username is invalid; print the reason
$messages = $validator->getMessages();
foreach ($messages as $message) {
echo "$message\n";
}
}
Now my question is how can do the validation part?
For example, I need to validate a name before inserting into database to check that the same name does not exist in the database, I have updated Zend Framework 2 example Album module to use Doctrine 2 to communicate with the database and right now I want to add the validation part to my code.
Let us say that before adding the album name to the database I want to validate that the same album name does not exist in the database.
Any information regarding this would be really helpful!
if you use the DoctrineModule, there is already a validator for your case.
I had the same problem and solved it this way:
Create a custom validator class, name it something like NoEntityExists (or whatever you want).
Extend Zend\Validator\AbstractValidator
Provide a getter and setter for Doctrine\ORM\EntityManager
Provide some extra getters and setters for options (entityname, ...)
Create an isValid($value) method that checks if a record exists and returns a boolean
To use it, create a new instance of it, assign the EntityManager and use it just like any other validator.
To get an idea of how to implement the validator class, check the validators that already exist (preferably a simple one like Callback or GreaterThan).
Hope I could help you.
// Edit: Sorry, I'm late ;-)
So here is a quite advanced example of how you can implement such a validator.
Note that I added a translate() method in order to catch language strings with PoEdit (a translation helper tool that fetches such strings from the source codes and puts them into a list for you). If you're not using gettext(), you can problably skip that.
Also, this was one of my first classes with ZF2, I wouldn't put this into the Application module again. Maybe, create a new module that fits better, for instance MyDoctrineValidator or so.
This validator gives you a lot of flexibility as you have to set the query before using it. Of course, you can pre-define a query and set the entity, search column etc. in the options. Have fun!
<?php
namespace Application\Validator\Doctrine;
use Zend\Validator\AbstractValidator;
use Doctrine\ORM\EntityManager;
class NoEntityExists extends AbstractValidator
{
const ENTITY_FOUND = 'entityFound';
protected $messageTemplates = array();
/**
* #var EntityManager
*/
protected $entityManager;
/**
* #param string
*/
protected $query;
/**
* Determines if empty values (null, empty string) will <b>NOT</b> be included in the check.
* Defaults to true
* #var bool
*/
protected $ignoreEmpty = true;
/**
* Dummy to catch messages with PoEdit...
* #param string $msg
* #return string
*/
public function translate($msg)
{
return $msg;
}
/**
* #return the $ignoreEmpty
*/
public function getIgnoreEmpty()
{
return $this->ignoreEmpty;
}
/**
* #param boolean $ignoreEmpty
*/
public function setIgnoreEmpty($ignoreEmpty)
{
$this->ignoreEmpty = $ignoreEmpty;
return $this;
}
/**
*
* #param unknown_type $entityManager
* #param unknown_type $query
*/
public function __construct($entityManager = null, $query = null, $options = null)
{
if(null !== $entityManager)
$this->setEntityManager($entityManager);
if(null !== $query)
$this->setQuery($query);
// Init messages
$this->messageTemplates[self::ENTITY_FOUND] = $this->translate('There is already an entity with this value.');
return parent::__construct($options);
}
/**
*
* #param EntityManager $entityManager
* #return \Application\Validator\Doctrine\NoEntityExists
*/
public function setEntityManager(EntityManager $entityManager)
{
$this->entityManager = $entityManager;
return $this;
}
/**
* #return the $query
*/
public function getQuery()
{
return $this->query;
}
/**
* #param field_type $query
*/
public function setQuery($query)
{
$this->query = $query;
return $this;
}
/**
* #return \Doctrine\ORM\EntityManager
*/
public function getEntityManager()
{
return $this->entityManager;
}
/**
* (non-PHPdoc)
* #see \Zend\Validator\ValidatorInterface::isValid()
* #throws Exception\RuntimeException() in case EntityManager or query is missing
*/
public function isValid($value)
{
// Fetch entityManager
$em = $this->getEntityManager();
if(null === $em)
throw new Exception\RuntimeException(__METHOD__ . ' There is no entityManager set.');
// Fetch query
$query = $this->getQuery();
if(null === $query)
throw new Exception\RuntimeException(__METHOD__ . ' There is no query set.');
// Ignore empty values?
if((null === $value || '' === $value) && $this->getIgnoreEmpty())
return true;
$queryObj = $em->createQuery($query)->setMaxResults(1);
$entitiesFound = !! count($queryObj->execute(array(':value' => $value)));
// Set Error message
if($entitiesFound)
$this->error(self::ENTITY_FOUND);
// Valid if no records are found -> result count is 0
return ! $entitiesFound;
}
}

Categories