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
Related
I'm building a Symfony 2 / Doctrine 2 application on top of an existing MySQL database. Due to poor decisions in the past, i'm stuck with references that are concatenated in a table-column. Unfortunately remodeling the database is not an option.
E.g. entity "Product" referring to multiple "Categories":
| id | name | category_ids |
|----|-----------|--------------|
| 1 | product a | 1,2,5 |
| 2 | product b | 3,4,1 |
| 3 | product c | 2 |
I would like to have the method getCategories available in my "Product" entity which would return a Collection of Category objects.
Is there any way to achieve this with Doctrine?
Maybe use custom code that is based on "FIND_IN_SET"?
SELECT c.*
FROM product p
LEFT OUTER JOIN category c ON FIND_IN_SET(c.id, p.category_ids)
WHERE p.id=:product_id;
Or maybe define the association with exploded values?
explode(',',$this->category_ids)
I try to avoid having to use the EntityManager each time i need to retrieve Categories from my Product entity. Because:
Injecting the EntityManager in Entities is bad practice
Using the EntityManager each time in my controllers a bit against the DRY-principle
I have no idea how to achieve this in Symfony FormTypes to have a Choice/Entity field with the relevant Categories for a Product.
Solution 1
You can make a hydration strategy for your getCategories method and register this strategy inside your hydrator class (could even be DoctrineObject hydrator). Something like:
Strategy
<?php
namespace My\Hydrator\Strategy;
use Doctrine\Common\Collections\ArrayCollection;
use My\Entity\Category;
use Doctrine\Common\Persistence\ObjectManager;
use DoctrineModule\Persistence\ObjectManagerAwareInterface;
use Zend\Stdlib\Hydrator\Strategy\StrategyInterface;
class CategoriesStrategy implements StrategyInterface, ObjectManagerAwareInterface
{
/**
* #var ObjectManager
*/
protected $objectManager;
/**
* #param ObjectManager $objectManager
* #param String $hostName
*/
public function __construct(ObjectManager $objectManager)
{
$this->objectManager = $objectManager;
}
/**
* #param array $value
* #return ArrayCollection
*/
public function extract($value)
{
$collection = new ArrayCollection();
if (is_array($value)) {
foreach ($value as $id) {
$category = $this->getObjectManager()->find(Category::class, $id);
$collection->add($category);
}
}
return $collection;
}
/**
* #param ArrayCollection $value
* #return array
*/
public function hydrate($value)
{
$array = array();
/** #var Category $category */
foreach ($value as $category) {
$array[] = $category->getId();
}
return $array;
}
/**
* #param ObjectManager $objectManager
* #return $this
*/
public function setObjectManager(ObjectManager $objectManager)
{
$this->objectManager = $objectManager;
return $this;
}
/**
* #return ObjectManager
*/
public function getObjectManager()
{
return $this->objectManager;
}
}
You probably need a factory to register your CategoriesStrategy inside your hydrator class:
Hydrator Factory
<?php
namespace My\Hydrator;
use Doctrine\Common\Persistence\ObjectManager;
use DoctrineModule\Stdlib\Hydrator\DoctrineObject;
use My\Hydrator\Strategy\CategoriesStrategy;
use Zend\ServiceManager\FactoryInterface;
use Zend\ServiceManager\ServiceLocatorInterface;
use Zend\ServiceManager\ServiceManager;
class MyHydratorFactory implements FactoryInterface
{
/**
* #param ServiceLocatorInterface $serviceLocator
* #return DoctrineObject
*/
public function createService(ServiceLocatorInterface $serviceLocator)
{
/** #var ServiceManager $serviceManager */
$serviceManager = $serviceLocator->getServiceLocator();
/** #var ObjectManager $objectManager */
$objectManager = $serviceManager->get('bc_object_manager');
/** #var DoctrineObject $hydrator */
$hydrator = new DoctrineObject($objectManager);
$hydrator->addStrategy('categories', new CategoriesStrategy($objectManager));
return $hydrator;
}
}
This is not tested, but you get the idea...
Solution 2
Another solution would be to register a DBAL type for your categories. You can check on how to do this in the Doctrine2 documentation chapter 8.4. Custom Mapping Types.
In your entity column definition you point to a categories type:
/**
* #var string
* #ORM\Column(type="categories")
*/
protected $categories;
And the magic you register in doctrine like this:
'doctrine' => array(
'configuration' => array(
'orm_default' => array(
'types' => array(
'categories' => 'My\DBAL\Types\CategoriesCollection '
)
)
)
)
And then the class itself:
<?php
namespace My\DBAL\Types;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\Common\Collections\Collection;
class CategoriesCollection extends \Doctrine\DBAL\Types\Type
{
const NAME = 'categories';
/**
* #return string
*/
public function getName()
{
return self::NAME;
}
/**
* {#inheritdoc}
*/
public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform)
{
return $platform->getDoctrineTypeMapping('simple_array');
}
/**
* #param Collection $collection
* #param AbstractPlatform $platform
* #return array
*/
public function convertToDatabaseValue($value, AbstractPlatform $platform)
{
$array = [];
foreach($value as $category)
{
$category_id = $category->getId();
array_push($array, $category_id);
}
return $array;
}
/**
* {#inheritdoc}
*/
public function convertToPHPValue($value, AbstractPlatform $platform)
{
$collection = new ArrayCollection();
if ($value === null) {
return $collection;
}
foreach($value as $category_id){
$category = $this->em->getReference('Vendor\Bundle\Entity\Category', $category_id);
$collection->add($category);
}
return $collection;
}
/**
* #var EntityManager
*/
protected $em;
/**
* #param EntityManager $entityManager
*/
public function setEntityManager(EntityManager $entityManager)
{
$this->em = $entityManager;
}
}
This solution is virtually the same as the other solution only you use the Doctrine2 internals to get there. You will still need to register the EntityManager in your DBAL type and not sure what is the easiest way, so that I leave up to you.
In Symfony you can register the Custom Mapping Type in your app/config/config.yml file
doctrine:
dbal:
types:
category_ids: Vendor\Bundle\Type\CategoriesCollection
You can than inject the EntityManager dependency in the boot sequence of your bundle:
<?php
namespace Vendor\Bundle\Bundle;
use Doctrine\DBAL\Types\Type;
use Symfony\Component\HttpKernel\Bundle\Bundle;
use Trilations\TApp\CoreBundle\Type\CategoryCollectionType;
class VendorBundleBundle extends Bundle
{
public function boot()
{
$em = $this->container->get('doctrine.orm.default_entity_manager');
$categoryCollectionType = Type::getType('category_ids');
$categoryCollectionType->setEntityManager($em);
}
}
And map the field to the correct custom mapping:
Vendor\Bundle\Enitity\Product
table: product
fields:
categories: category_ids
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.
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()
));
When I programmed in ASP.NET MVC, there was a neat pattern called Repository. I want to implment it in Codeigniter but I do not know how. Here is what I actually want:
$mock_repository = new MockRepository();
$mock_repository->add(new Item(‘title1′, ‘description1′, 1));
$mock_repository->add(new Item(‘title2′, ‘description2′, 2));
$mock_repository->add(new Item(‘title3′, ‘description3′, 1));
$controller = new Item_controller($mock_repository);
$items = $controller->get_items_by_user_id(1);
$this->_assert_equals(count($items), 2);
I am using TOAST for Unit Testing. So how do I instantiate a controller within a test? The test is of course, another controller itself.
From what I know, to create a Generic Repository Pattern like in C#, you need 2 things PHP 5.6 dosen't have:
Real Method Overloading.
Generic Interface or Generic Abstract Class in PHP.
Click here for more on Generic Repository Pattern in C#.
However you can still create pseudo method overloading in PHP with the help of magic method __call, and we can type little more code for the generic part of the pattern.
Note: Before creating this pattern in Codeigniter 3.0 you will need to create a table in the database, and create auto loader for folder application/libraries.
First we need to create Interface in application/libraries folder:
<?php
interface IRepository
{
public function getById($id);
public function select($columns);
public function delete($id);
}
Seconde we need to create Abstract Class implementing the Interface and extending the CI_Model to be able to use the Database librarie:
<?php
abstract class Base_repository extends CI_Model implements IRepository
{
/**
* This must be valid table name in the Database.
*
* #var string $table Name of the table.
*/
protected $table;
public function __construct()
{
parent::__construct();
}
/**
* Pseudo method overloading.
* It's called when method is not declared in the abstract class.
*
* #param string $name Name of the method
* #param mixed $arguments Arguments of the method
*/
public function __call($name, $arguments)
{
switch ($name)
{
case 'save':
if ($arguments[0]->id > 0)
{
$this->update($arguments[0]);
}
else
{
$this->insert($arguments[0]);
}
break;
}
}
/**
* Get row with id.
*
* #param integer $id
* #return mixed
*/
public function getById($id)
{
return $this->db->get_where($this->table, ['id' => $id])->row_array();
}
/**
* Select columns.
*
* #param array $columns
* #return mixed
*/
public function select($columns = ['*'])
{
$this->db->select($columns);
return $this->db->get($this->table)->result();
}
/**
* Insert data.
*
* #param object $item
* #return void
*/
private function insert($item)
{
unset($item->id);
$this->db->insert($this->table, $item);
}
/**
* Update data.
*
* #param object $item
* #return void
*/
private function update($item)
{
$this->db->where('id =', $item->id);
unset($item->id);
$this->db->update($this->table, $item);
}
/**
* Delete data.
*
* #param integer $id
* #return void
*/
public function delete($id)
{
$this->db->delete($this->table, ['id' => $id]);
}
}
Third test the repository. Make a new model in application/model, and extend Base_repository, set table name and overload save method, create entity for this model:
<?php
/**
* The entity class.
*/
class Test
{
public $id;
public $info;
}
class Test_model extends Base_repository
{
/**
* Tell what table we are using.
*/
public function __construct()
{
parent::__construct();
$this->table = 'test';
}
/**
* "Overload" save method and call it from the parent.
*
* #param test $item Make use of the Dependency Injection.
* #return void
*/
public function save(Test $item)
{
parent::save($item);
}
}
Try it in the controller. Load the model and try to get, insert, ect...
To create real models is the same procedure. If you need to add more methods that will be the same for every model add them in the abstract class if you need to create methods only for specific model add it only in this model.
I don't recommend Codeigniter freamwork. Here are some patterns for PHP CLICK!
You would have to completely hijack the system files to load a controller from another controller. It can't be done, methinks.
It can be done with HMVC.
$result = Modules::run('controller/get_items_by_user_id', $params);
$this->_assert_equals($result, $expected);
Suppose i'm having the following Doctrine 2 entity:
/**
* #ORM\Entity
* #ORM\Table(name="users")
*/
class User {
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue
*
* #var int
*/
protected $id;
/**
* #ORM\Column(length=100)
*
* #var string
*/
protected $name;
/**
* #ORM\Column(type="integer")
*
* #var int
*/
protected $status;
}
The User can have several statuses, for example: Pending, Active, Suspended. These statuses are needed throughout the code (services, repositories, etc.) and also in the UI layer (a User edit form would display them in a dropdown).
In order to avoid defining them in multiple places, what i've done so far was to use a class to hold them all (all the application's constants), and it looks somewhat like this:
class App_Constants extends Zrzr_Constants
{
protected static $_constants = array(
'users' => array(
'status' => array(
0 => 'Pending',
1 => 'Active',
2 => 'Suspended' ) ) );
}
The base class (Zrzr_Constants) would offer some methods to retrieve them, and it looks like this:
class Zrzr_Constants
{
protected static $_constants = array();
public static function getConstantValues( $key, $subkey )
{
// ...
}
public static function getConstantByName( $name )
{
// ...
}
}
Common usage would be:
// example of retrieval by constant name ... it would return an integer
$pendingStatus = App_Constants::getConstantByName( 'USERS.STATUS.PENDING' );
// example of retrieval for UI display purposes ... would return an array
$statuses = App_Constants::getConstantValues('users', 'status');
Of course this means that there are some limitations in that the constant labels cannot contain dots, but i can live with it.
Using Doctrine 2 and going the DDD way however, tells me that the 'status' field should be in fact a 'value object' (but Doctrine 2 does not support value objects yet), or at least that i should have the constants defined within the entity (using const).
My question is how would i do this so that i avoid constant redefinition for the UI layer? I need to have access to the constant by name (in the code) and to have all the possible values for such a field in the case of a UI dropdown (for example).
I think, you can do it this way:
class User {
const STATUS_PENDING = 'Pending';
const STATUS_ACTIVE = 'Active';
const STATUS_SUSPENDED = 'Suspended';
public static function getStatusList() {
return array(
self::STATUS_PENDING,
self::STATUS_ACTIVE,
self::STATUS_SUSPENDED
);
}
public function getStatus() {...}
public function setStatus($value) {...}
public function isStatusPending() {...} //If you need it
}
On the UI layer, you can get text versions of your statuses using localization service (if status constants are numbers, UI layer can convert them to strings by adding prefix, for example user_status_0). In Symfony2 views you can use trans Twig filter for that to get text version of user status from user localization domain.
If your website is just in one language, then just User::STATUS_XXX will do fine, I think. I don't think you should overcomplicate the matter by creating a new class to hold statuses of the user.
If you will end up having many statuses or some other related things, I think you will have to create a separate entity for them.
you can define your class as in the following example
class ContactResource
{
const TYPE_PHONE = 1;
const TYPE_EMAIL = 2;
const TYPE_BIRTDAY = 3;
const TYPE_ADDRESS = 4;
const TYPE_OTHER = 5;
const TYPE_SKYPE = 6;
const TYPE_LINKEDIN = 7;
const TYPE_MEETUP = 8;
const TYPE_TELEGRAM = 9;
const TYPE_INSTAGRAM = 10;
const TYPE_TWITTER = 11;
public static $resourceType = array(
ContactResource::TYPE_PHONE => "Phone",
ContactResource::TYPE_EMAIL => "Email",
ContactResource::TYPE_BIRTDAY => "Birtday",
ContactResource::TYPE_ADDRESS => "Address",
ContactResource::TYPE_OTHER => "Other",
ContactResource::TYPE_SKYPE => "Skype",
ContactResource::TYPE_LINKEDIN => "LinkedIn",
ContactResource::TYPE_MEETUP => "Meetup",
ContactResource::TYPE_TELEGRAM => "Telegram",
ContactResource::TYPE_INSTAGRAM => "Instagram",
ContactResource::TYPE_TWITTER => "Twitter",
);
/**
* #var integer
*
* #ORM\Column(type="integer", length=2)
*
*/
private $type;
public function __toString()
{
return (string)$this->getType();
}
public function getType()
{
if (!is_null($this->type)) {
return self::$resourceType[$this->type];
} else {
return null;
}
}
public static function getTypeList() {
return self::$resourceType;
}
}
If you need to get the type in Twig
{{ entity.type }}
For the list of choices
ContactResource::getTypeList()
Hope works for you!
Several years later and some more experience, what I consider to be the proper answer has changed. The initial question is about domain constants used in the UI layer, but the given example and the discussions actually refer to the following concepts: enums, enum maps and value objects. I did not have these concepts back then and the answers to my question did not provide them.
When you see or think of constants like STATUS_PENDING, STATUS_ACTIVE, STATUS_SUSPENDED you should be thinking of an enum. The standard PHP enum is insufficient so I like to use a third party library like marc-mabe/php-enum. Here's how it would look like:
use MabeEnum\Enum;
/**
* #method static UserStatus PENDING()
* #method static UserStatus ACTIVE()
* #method static UserStatus SUSPENDED()
*/
class UserStatus extends Enum
{
const PENDING = 0;
const ACTIVE = 1;
const SUSPENDED = 2;
}
It's easy to turn this into a value object if you need to add functionality to it (I recommend doing it through composition, not inheritance). Coming back to the User entity, using the above enum the entity would end up like this:
/**
* #ORM\Entity
* #ORM\Table(name="users")
*/
class User {
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue
*
* #var int
*/
protected $id;
/**
* #ORM\Column(length=100)
*
* #var string
*/
protected $name;
/**
* #ORM\Column(type="user_status")
*
* #var UserStatus
*/
protected $status;
}
Notice the column type is "user_status". To get this to work you need to define a custom Doctrine type and register it with Doctrine. Such a type would look like this:
/**
* Field type mapping for the Doctrine Database Abstraction Layer (DBAL).
*
* UserStatus fields will be stored as an integer in the database and converted back to
* the UserStatus value object when querying.
*/
class UserStatusType extends Type
{
/**
* #var string
*/
const NAME = 'user_status';
/**
* {#inheritdoc}
*
* #param array $fieldDeclaration
* #param AbstractPlatform $platform
*/
public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform)
{
return $platform->getIntegerTypeDeclarationSQL($fieldDeclaration);
}
/**
* {#inheritdoc}
*
* #param string|null $value
* #param AbstractPlatform $platform
*/
public function convertToPHPValue($value, AbstractPlatform $platform)
{
if (empty($value)) {
return null;
}
if ($value instanceof UserStatus) {
return $value;
}
try {
$status = UserStatus::get((int)$value);
} catch (InvalidArgumentException $e) {
throw ConversionException::conversionFailed($value, self::NAME);
}
return $status;
}
/**
* {#inheritdoc}
*
* #param UserStatus|null $value
* #param AbstractPlatform $platform
*/
public function convertToDatabaseValue($value, AbstractPlatform $platform)
{
if (empty($value)) {
return null;
}
if ($value instanceof UserStatus) {
return $value->getValue();
}
throw ConversionException::conversionFailed($value, self::NAME);
}
/**
* {#inheritdoc}
*
* #return string
*/
public function getName()
{
return self::NAME;
}
/**
* {#inheritdoc}
*
* #param AbstractPlatform $platform
*
* #return boolean
*/
public function requiresSQLCommentHint(AbstractPlatform $platform)
{
return true;
}
}
Finally, when it comes to satisfying the needs of the user interface, you can end up using enum maps. Remember that the UI could need additional functionality such as multiple language support, so you cannot mash such concerns into the domain, hence the separation:
use MabeEnum\EnumMap;
class UserStatusMap extends EnumMap
{
public function __construct()
{
parent::__construct(UserStatus::class);
$this[UserStatus::PENDING] = ['name' => 'Pending'];
$this[UserStatus::ACTIVE] = ['name' => 'Active'];
$this[UserStatus::SUSPENDED] = ['name' => 'Suspended'];
}
}
You can just add as many keys you want beside 'name'. In the UI you can make use of such a map like this:
// if you want to display the name when you know the value
echo (new UserStatusMap ())[UserStatus::PENDING]['name'];
// or
echo (new UserStatusMap ())[UserStatus::PENDING()]['name'];
// if you want to build a list for a select (value => name)
$list = (new UserStatusMap ())->toArray('name');
The toArray function is not available in MabeEnum\EnumMap but you can make your own:
use MabeEnum\EnumMap as BaseEnumMap;
class EnumMap extends BaseEnumMap
{
/**
* #param string|null $metadataKey
*
* #return array
*/
public function toArray($metadataKey = null)
{
$return = [];
$flags = $this->getFlags();
$this->setFlags(BaseEnumMap::KEY_AS_VALUE | BaseEnumMap::CURRENT_AS_DATA);
if ($metadataKey) {
foreach ($this as $key => $value) {
$return[$key] = $value[$metadataKey];
}
} else {
$return = iterator_to_array($this, true);
}
$this->setFlags($flags);
return $return;
}
}
To summarize:
Use an Enum to define a list of alternative values for a single field.
Create a Value Object which receives this Enum in the constructor if you want to add VO specific functionality to this field.
Use an Enum Map to serve the UI needs.