Dynamically map manyToOne relations - symfony

I'm trying to create Symfony Bundle in which entities are defined which can be used in a one-to-many/many-to-one relationship without the needing to rewrite the mapping manually.
I do this by subscribing to the loadClassMetadata event and adding the mapping based on the Interfaces they implement. It is not as simple as using the ResolveTargetEntityListener because that will simply substitute an interface with the concrete class.
An example. I have a Address and a Customer entity. A Customer has many Addresses.
But another bundle may redefine the Customer (or a totally different Entity which can have multiple Addresses). For this reason the Customer implements the AddressableInterface. For ease of use I've implemented this interface in a trait.
In the subscriber I check if the class implements the AddressableInterface. If so it adds an OneToMany to the Address and an ManyToOne to the class which implements the AddressableInterface. (In this example the Customer class)
However this leaves the following error:
The association Entity\Customer#addresses refers to the owning side field Entity\Address#subject which does not exist.
But I setup to association both ways in my subscriber.
Below is the essence of my code.
namespace Entity;
class Address
{
public $subject;
}
namespace Entity;
class Customer implements AddressableInterface
{
use Traits/Addressable;
}
namespace Traits;
trait Addressable //Implements all methods from AddressableInterface
{
protected $addresses;
public function getAddresses()
{
return $this->addresses;
}
public function addAddress(AddressInterface $address)
{
$this->addresses->add($address);
}
public function removeAddress(AddressInterface $address)
{
$this->addresses->removeElement($address);
}
}
And the event subscriber
class DynamicAddressBindingSubscriber implements EventSubscriber
{
public function getSubscribedEvents()
{
return [Events::loadClassMetadata];
}
public function loadClassMetadata(LoadClassMetadataEventArgs $eventArgs)
{
$metadata = $eventArgs->getClassMetadata();
$class = $metadata->getReflectionClass();
if (!in_array(AddressableInterface::class, $class->getInterfaceNames())) {
return;
}
$factory = new \Doctrine\ORM\Mapping\ClassMetadataFactory;
$factory->setEntityManager($eventArgs->getEntityManager());
$addressMetadata = $factory->getMetadataFor(Address::class);
$addressMetadata->mapManyToOne(
[
"targetEntity" => $class->getName(),
"fieldName" => "subject",
"inversedBy" => "addresses"
]
);
$metadata->mapOneToMany(
[
'targetEntity' => Address::class,
'fieldName' => 'addresses',
'mappedBy' => 'subject'
]
);
}
}
I've looked at multiple examples and based most of my code on this article and the Doctrine Bundle source. But I'm stuck at this point because I have no idea why the association can't find the owing side.

Your address class doesn't have getter/setter for the subject field.
Another thing is that if you want to bind addresses to any class, you might prefer to make it a manyToMany relations. I do so with attachments like this:
$metadata->mapManyToMany([
'targetEntity' => '...\FilesBundle\Entity\Attachment',
'fieldName' => 'attachments',
'cascade' => array('persist'),
'joinTable' => array(
'name' => strtolower($namingStrategy->classToTableName($metadata->getName())) . '_attachment',
'joinColumns' => array(
array(
'name' => $namingStrategy->joinKeyColumnName($metadata->getName()),
'referencedColumnName' => $namingStrategy->referenceColumnName(),
'onDelete' => 'CASCADE',
'onUpdate' => 'CASCADE',
),
),
'inverseJoinColumns' => array(
array(
'name' => 'file_id',
'referencedColumnName' => $namingStrategy->referenceColumnName(),
'onDelete' => 'CASCADE',
'onUpdate' => 'CASCADE',
),
)
)
]);
where namingStrategy comes from the event:
$namingStrategy = $eventArgs
->getEntityManager()
->getConfiguration()
->getNamingStrategy()
;

Related

Customising filter for ModelAdmin to support date range in SilverStripe

I am developing a SilverStripe project. I am now struggling with customizing the filter/ search for the ModelAdmin entities, https://silverstripe.org/learn/lessons/v4/introduction-to-modeladmin-1. I am trying to add a date range filter as follows.
As you can see there are from and to fields. I have a class called Property and I am trying to customize the search/ filter for the CMS as follow to support the date range filtering.
class Property extends DataObject
{
public function searchableFields()
{
return [
//other fields go here
'Created' => [
'filter' => 'GreaterThanOrEqualFilter',
'title' => 'From',
'field' => DateField::class
],
'Created' => [
'filter' => 'To',
'title' => 'Decision date until',
'field' => DateField::class
],
];
}
}
Only one field is added to the pop up because the array key is overridden. How can I configure it to have the two date fields to specify the date range for the search form?
It might not be relevant now but I bumped to this issue today and I tried your code, you are right, only one field is created I think because you are using single DateField::class. I tried to look for a module that creates a Date Range field and I only can find this one but it looks like it's a project specific.
In my case I have 2 date fields (created and ended), using your code I can get good results by tweaking it to something like this:
public function searchableFields()
{
return [
//other fields go here
'StartDate' => [
'filter' => 'GreaterThanOrEqualFilter',
'title' => 'From',
'field' => DateField::class
],
'EndDate' => [
'filter' => 'LessThanOrEqualFilter',
'title' => 'To',
'field' => DateField::class
],
];
}
Hope it helps someone.
Using this example DataObject create a custom update function updateAdminSearchFields...
app/src/Test/MyDataObject.php
namespace MyVendor\MyNamespace;
use SilverStripe\Forms\DateField;
use SilverStripe\ORM\DataObject;
class MyDataObject extends DataObject {
private static $db = [
'Title' => 'Varchar',
'MyDateTimeField' => 'DBDatetime'
];
private static $summary_fields = ['Title','MyDateTimeField'];
public function updateAdminSearchFields($fields) {
$fields->removeByName('MyDateTimeField');//needed as added in summary field
$fields->push(DateField::create('MyDateTimeField:GreaterThanOrEqual', 'MyDateTimeField (Start)'));
$fields->push(DateField::create('MyDateTimeField:LessThanOrEqual', 'MyDateTimeField (End)'));
}
}
Then create an extension that can link that to a ModelAdmin...
app/src/Test/MyAdminExtension.php
namespace MyVendor\MyNamespace;
use SilverStripe\ORM\DataExtension;
class MyAdminExtension extends DataExtension {
public function updateSearchContext($context) {
$class = $context->getQuery([])->dataClass();
if (method_exists($class, 'updateAdminSearchFields'))
(new $class)->updateAdminSearchFields($context->getFields());
return $context;
}
}
app/_config/mysite.yml
MyVendor\MyNamespace\MyAdmin:
extensions:
- MyVendor\MyNamespace\MyAdminExtension
Lastly on the ModelAdmin apply these filters...
app/src/Test/MyAdmin.php
namespace MyVendor\MyNamespace;
use SilverStripe\Admin\ModelAdmin;
class MyAdmin extends ModelAdmin {
private static $menu_title = 'MyAdmin';
private static $url_segment = 'myadmin';
private static $managed_models = [MyDataObject::class];
public function getList() {
$list = parent::getList();
if ($params = $this->getRequest()->requestVar('filter'))
if ($filters = $params[$this->sanitiseClassName($this->modelClass)])
return $list->filter($filters);
return $list;
}
}
This example is working on latest stable version 4.7.2

How to modify Product in Silvershop (adding custom fields to $db)

I'm currently developing a shop using SilverShop. I want to add some specific fields to my products, such as what fabric my clothes are made of and an image. I know that we should not make these changes in the core SilverShop source code.
Should I extend the Product class in a new file such as app/src/ProductPage.php?
class Product extends Page implements Buyable
{
private static $db = [
'InternalItemID' => 'Varchar(30)', //ie SKU, ProductID etc (internal / existing recognition of product)
'Model' => 'Varchar(30)',
'BasePrice' => 'Currency(19,4)', // Base retail price the item is marked at.
//physical properties
// TODO: Move these to an extension (used in Variations as well)
'Weight' => 'Decimal(12,5)',
'Height' => 'Decimal(12,5)',
'Width' => 'Decimal(12,5)',
'Depth' => 'Decimal(12,5)',
'Featured' => 'Boolean',
'AllowPurchase' => 'Boolean',
'Popularity' => 'Float' //storage for CalculateProductPopularity task
];
...
Use DataExtension
For SilverStripe 4, it will be something like:
ProductExtension.php :
use SilverStripe\ORM\DataExtension;
use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\TextField;
class ProductExtension extends DataExtension
{
private static $db = [
'NewField' => 'Varchar(255)'
];
public function updateCMSFields(FieldList $fields)
{
$fields->addFieldsToTab('Root.Main', TextField::create('NewField', 'This is new field'));
}
}
And, add the next lines to mysite.yml
SilverShop\Page\Product:
extensions:
- ProductExtension
dev/build and it's done

Using objects inside sonata_type_immutable_array (especially like sonata_type_immutable_array)

Problem description
I need to define object-related field (like sonata_type_model_list) inside sonata_type_immutable_array form type definition:
$formMapper->add('options', 'sonata_type_immutable_array', array(
'keys' => array(
array('linkName', 'text', array()),
array('linkPath', 'sonata_type_model_list',
array(
'model_manager' => $linkAdmin->getModelManager(),
'class' => $linkAdmin->getClass(),
)
)
)
)
This is not working, here is error message:
Impossible to access an attribute ("associationadmin") on a NULL variable ("") in SonataDoctrineORMAdminBundle:Form:form_admin_fields.html.twig at line 60
What I found about this problem
I tryed to find any information about using sonata_type_model_list inside sonata_type_immutable_array, but there is very little information.
This (https://github.com/a2lix/TranslationFormBundle/issues/155) topic helped me a bit, but doing all in the same manner I've got another error:
Impossible to invoke a method ("id") on a NULL variable ("") in SonataDoctrineORMAdminBundle:Form:form_admin_fields.html.twig at line 60
So I totally failed in uderstanding what I have to do.
My context
-- I have Doctrine ORM Mapped class called CmsLink, it defines object to which 'linkPath' field relates.
-- I have admin class for CmsLink class, it has very basic configuration:
protected function configureFormFields(FormMapper $formMapper)
{
$formMapper
->add('technicalAlias')
->add('url')
;
}
-- I have Doctrine ORM Mapped class called CmsMenuItem, it defines object and 'options' filed which persists data managed by sonata_type_immutable_array form type, field type is json_array:
/**
* #var string
*
* #ORM\Column(name="options", type="json_array", nullable=true)
*/
private $options;
-- And finally I have admin class for CmsMenuItem class, here is the key code piece:
$linkAdmin = $this->configurationPool->getAdminByClass("Argon\\CMSBundle\\Entity\\CmsLink");
$formMapper
->add('options', 'sonata_type_immutable_array',
array(
'keys' => array(
array('linkName', 'text', array()),
array('linkPath', 'sonata_type_model_list',
array(
'model_manager' => $linkAdmin->getModelManager(),
'class' => $linkAdmin->getClass(),
)
),
array('description', 'textarea', array()),
array('image', 'sonata_media_type',
array(
'provider' => 'sonata.media.provider.image',
'context' => 'pages_static',
'required'=>false,
)
)
)
)
);
Question goals
Find out what I need to do to bring life into this idea?
Get general information and understanding abot how to include object-related field types into sonata_type_immutable_array
I've just come across this problem, and solved it with a custom type and data transformers.
Here's the rough outline, though you need to tailor it to your problem.
Custom Type
class YourImmutableArrayType extends ImmutableArrayType
{
/**
* #var YourSettingsObjectTransformer
*/
private $transformer;
public function __construct(YourSettingsObjectTransformer $transformer)
{
$this->transformer = $transformer;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
parent::buildForm($builder, $options);
$builder->addModelTransformer($this->transformer);
}
public function getName()
{
return 'your_type_name';
}
public function getParent()
{
return 'sonata_type_immutable_array';
}
}
Custom model transformer
class NewsListingSettingsTransformer implements DataTransformerInterface
{
public function __construct(ObjectManager $manager)
{
// You'll need the $manager to lookup your objects later
}
public function reverseTransform($value)
{
if (is_null($value)) {
return $value;
}
// Here convert your objects in array to IDs
return $value;
}
public function transform($value)
{
if (is_null($value)) {
return $value;
}
// Here convert ids embedded in your array to objects,
// or ArrayCollection containing objects
return $value;
}
}
 Building form in admin class
$formMapper->add('settings', 'your_type_name', array(
'keys' => array(
array(
'collectionOfObjects',
'sonata_type_model',
array(
'class' => YourObject::class,
'multiple' => true,
'model_manager' => $this->yourObjectAdmin->getModelManager()
)
)
)
));
}
Again, it's a rough outline, so tweak it to your needs.

how to call function of entity repository in form type in symfony2

i want to call function in form type class. function generate array and is written in entity repository class. using that array i will generate dynamic form field.
here is entity repository class function.
public static $roleNameMap = array(
self::ROLE_SUPER_ADMIN => 'superAdmin',
self::ROLE_MANAGEMEN => 'management',
self::ROLE_MANAGERS => 'manager',
self::ROLE_IT_STAFF => 'itStaff',
self::ROLE_CS_CUSTOMER => 'csCustomer',
self::ROLE_CS => 'cs',
self::ROLE_DEALER => 'dealer',
self::ROLE_ACCOUNT_STAFF => 'accountStaff',
self::ROLE_BROKER_USER => 'staff',
);
public function getGroupListArray()
{
$qb = $this->createQueryBuilder('g')
->orderBy('g.hierarchy','ASC');
$query = $qb->getQuery();
$groupList = $query->execute();
$roleNameMap = array();
foreach ($groupList as $role){
$roleNameMap[$role->getId()] = $role->getRole();
}
return $roleNameMap;
}
below is my form builder class where i want to call above entity repository function.
public function buildForm(FormBuilderInterface $builder, array $options) {
$builder->add('routeId', 'hidden');
foreach (GroupListRepository::$roleNameMap as $key=>$value){
$builder->add($value, 'checkbox',array('label' => '', 'required' => false,));
}
}
i am able to get static variable as show in above code but, i have confusion that how should i access repository function in form builder class in symfony2.
thanks in advance.
It's not available in the form builder, and it's normally not necessary. It's also not really how Symfony forms work. For what it looks like you're wanting to do, you could try something like this. It will create a list of checkboxes corresponding to a list of roles.
$builder->add(
'roles',
'entity',
array(
'class' => 'Acme\DefaultBundle\Entity\Group',
'expanded' => true,
'multiple' => true,
'property' => 'role', // Or use __toString()
'query_builder' => function ($repository) {
return $repository->createQueryBuilder('g')
->orderBy('g.hierarchy', 'ASC');
}
)
);
See http://symfony.com/doc/master/reference/forms/types/entity.html.
If you really need the repository in the form builder, then create the form type as a service and inject the entity manager with the DIC. Or just pass it directly into the form type when you create it.
You do not need to create a query builder function and can use a query from the Repository like so:
In the form:
'query_builder' => function(MyCustomEntityRepository $ttr) {
return $ttr->queryForCustomResultsWithQueryBuilder();
}
In the repository:
public function queryForCustomResultsWithQueryBuilder($published=true) {
$queryBuilder = $this->getEntityManager()->createQueryBuilder();
return $queryBuilder->select('tt')
->from('ifm\CustomBundle\Entity\CustomEntity','tt')
->where('tt.published = ?1')
->orderBy('tt.code', 'ASC')
->setParameters(array(1=>$published))
;
}
Note that the queryForCustomResultsWithQueryBuilder returns a QueryBuilder not a result. If you also need a result you'll need to write a find function in the reposiory.

How do I test my Zend Framework 2 InputFilter with PHPUnit?

I have a basic ZF2 InputFilter that I created. How exactly do I test it with PHPUnit without attaching it to a Form?
I can't find any sample on how this is done. Hope someone can help out.
I usually have a data provider to test my input filters.
Here's an example input filter with two really simple fields:
use Zend\InputFilter\InputFilter;
use Zend\InputFilter\Input;
use Zend\I18n\Validator\Alnum;
class MyInputFilter extends InputFilter
{
public function __construct()
{
$name = new Input('name');
$name->setRequired(false)->setAllowEmpty(true);
$this->add($name);
$nickname = new Input('nickname');
$nickname->getValidatorChain()->attach(new Alnum());
$this->add($nickname);
}
}
And here's a test class for it:
class MyInputFilterTest extends \PHPUnit_Framework_TestCase
{
public function setUp()
{
$this->inputFilter = new MyInputFilter();
}
/** #dataProvider validatedDataProvider */
public function testValidation($data, $valid)
{
$this->inputFilter->setData($data);
$this->assertSame($valid, $this->inputFilter->isValid());
}
public function validatedDataProvider()
{
return array(
array(
array(),
false
),
array(
array('name' => '', 'nickname' => 'Ocramius'),
true
),
array(
array('name' => 'Test', 'nickname' => 'Ocramius'),
true
),
array(
array('name' => 'Test', 'nickname' => 'Hax$or'),
false
),
);
}
}
This is a very simple example, but I am basically throwing different datasets at the filter and checking what is relevant to me (in this case checking that data is valid or invalid).
If your filter applies transformations on the data, you may also want to check what the output of $inputFilter->getValues() is.
If the error messages are relevant to you, you can also check $inputFilter->getMessages().

Resources