Skip to content

Commit 8763757

Browse files
committed
[PropertyAccess] Custom methods on property accesses
1 parent 10ea5e9 commit 8763757

File tree

1 file changed

+274
-1
lines changed

1 file changed

+274
-1
lines changed

components/property_access/introduction.rst

Lines changed: 274 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,9 @@ property name (``first_name`` becomes ``FirstName``) and prefixes it with
116116

117117
var_dump($accessor->getValue($person, 'first_name')); // 'Wouter'
118118

119+
You can override the called getter method using metadata (i.e. annotations or
120+
configuration files). see `Custom method calls and virtual properties in a class`_
121+
119122
Using Hassers/Issers
120123
~~~~~~~~~~~~~~~~~~~~
121124

@@ -306,6 +309,9 @@ see `Enable other Features`_.
306309
307310
var_dump($person->getWouter()); // array(...)
308311
312+
You can override the called setter method using metadata (i.e. annotations or
313+
configuration files). see `Custom method calls and virtual properties in a class`_
314+
309315
Checking Property Paths
310316
-----------------------
311317

@@ -365,8 +371,225 @@ You can also mix objects and arrays::
365371
var_dump('Hello '.$accessor->getValue($person, 'children[0].firstName')); // 'Wouter'
366372
// equal to $person->getChildren()[0]->firstName
367373

374+
Custom method calls and virtual properties in a class
375+
-----------------------------------------------------
376+
377+
Sometimes you may not want the component to guess which method has to be called
378+
when reading or writing properties. This is specially interesting when property
379+
names are not in English or its singularization is not properly detected.
380+
381+
For those cases you can add metadata to the class being accessed so that the
382+
component will use a particular method as a getter, setter or even adder and
383+
remover (for collections).
384+
385+
Another interesting use of custom methods is declaring virtual properties
386+
which are not stored directly in the object.
387+
388+
There are three supported ways to state this metadata supported out-of-the-box by
389+
the component: using annotations, using YAML configuration files or using XML
390+
configuration files.
391+
392+
.. caution::
393+
394+
When using as a standalone component the metadata feature is disabled by
395+
default. You can enable it by calling
396+
:method:`PropertyAccessorBuilder::setMetadataFactory
397+
<Symfony\\Component\\PropertyAccess\\PropertyAccessorBuilder::setMetadataFactory>`
398+
see `Enable other Features`_.
399+
400+
There are four method calls that can be overriden: `getter`, `setter`, `adder` and
401+
`remover`.
402+
403+
When using annotations you can precede a property with `@Property` to state which
404+
method should be called when a get, set, add or remove operation is needed on the
405+
property.
406+
407+
.. configuration-block::
408+
409+
.. code-block:: php
410+
411+
// ...
412+
use Symfony\Component\PropertyAccess\Annotation\Property;
413+
414+
class Person
415+
{
416+
/**
417+
* @Property(getter="getFullName", setter="setFullName")
418+
*/
419+
private $name;
420+
421+
/**
422+
* @Property(adder="addNewChild", remover="discardChild")
423+
*/
424+
private $children;
425+
426+
public function getFullName()
427+
{
428+
return $this->name;
429+
}
430+
431+
public function setFullName($fullName)
432+
{
433+
$this->name = $fullName;
434+
}
435+
}
436+
437+
.. code-block:: yaml
438+
439+
Person:
440+
name:
441+
getter: getFullName
442+
setter: setFullName
443+
children:
444+
adder: addNewChild
445+
remover: discardChild
446+
447+
.. code-block:: xml
448+
449+
<?xml version="1.0" ?>
450+
451+
<property-access xmlns="http://symfony.com/schema/dic/property-access-mapping"
452+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
453+
xsi:schemaLocation="http://symfony.com/schema/dic/property-access-mapping http://symfony.com/schema/dic/property-access-mapping/property-access-mapping-1.0.xsd">
454+
455+
<class name="Person">
456+
<property name="name" getter="getFullName" setter="setFullName" />
457+
<property name="children" adder="addNewChild" remover="discardChild" />
458+
</class>
459+
460+
</property-access>
461+
462+
Then, using the overriden methods is automatic:
463+
464+
.. code-block:: php
465+
466+
$person = new Person();
467+
468+
$accessor->setValue($person, 'name', 'John Doe');
469+
// will call setFullName
470+
471+
var_dump('Hello '.$accesor->getValue($person, 'name'));
472+
// will return 'Hello John Doe'
473+
474+
You can also associate a particular method with an operation on a property
475+
using the `@Getter`, `@Setter`, `@Adder` and `@Remover` annotations. All of them
476+
take only one parameter: `property`.
477+
478+
This allows creating virtual properties that are not directly stored in the
479+
object::
480+
481+
.. configuration-block::
482+
483+
.. code-block:: php
484+
485+
// ...
486+
use Symfony\Component\PropertyAccess\Annotation\Getter;
487+
use Symfony\Component\PropertyAccess\Annotation\Setter;
488+
489+
class Invoice
490+
{
491+
private $quantity;
492+
493+
private $pricePerUnit;
494+
495+
// Notice that there is no real "total" property
496+
497+
/**
498+
* @Getter(property="total")
499+
*/
500+
public function getTotal()
501+
{
502+
return $this->quantity * $this->pricePerUnit;
503+
}
504+
505+
/**
506+
* @Setter(property="total")
507+
*
508+
* @param mixed $total
509+
*/
510+
public function setTotal($total)
511+
{
512+
$this->quantity = $total / $this->pricePerUnit;
513+
}
514+
}
515+
516+
.. code-block:: yaml
517+
518+
Invoice:
519+
total:
520+
getter: getTotal
521+
setter: setTotal
522+
523+
.. code-block:: xml
524+
525+
<?xml version="1.0" ?>
526+
527+
<property-access xmlns="http://symfony.com/schema/dic/property-access-mapping"
528+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
529+
xsi:schemaLocation="http://symfony.com/schema/dic/property-access-mapping http://symfony.com/schema/dic/property-access-mapping/property-access-mapping-1.0.xsd">
530+
531+
<class name="Invoice">
532+
<property name="total" getter="getTotal" setter="setTotal" />
533+
</class>
534+
535+
</property-access>
536+
537+
.. code-block:: php
538+
539+
$invoice = new Invoice();
540+
541+
$accessor->setValue($invoice, 'quantity', 20);
542+
$accessor->setValue($invoice, 'pricePerUnit', 10);
543+
var_dump('Total: '.$accesor->getValue($invoice, 'total'));
544+
// will return 'Total: 200'
545+
546+
Using property metadata with Symfony
547+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
548+
549+
By default, Symfony will look for property metadata in the following places
550+
inside each bundle path:
551+
552+
- `<Bundle path>/Resources/config/property_access.xml`
553+
- `<Bundle path>/Resources/config/property_access.yml`
554+
- `<Bundle path>/Resources/config/property_access/*.xml`
555+
- `<Bundle path>/Resources/config/property_access/*.yml`
556+
557+
If you need getting metadata from annotations you must explicitly enable them:
558+
559+
.. configuration-block::
560+
561+
.. code-block:: yaml
562+
563+
# app/config/config.yml
564+
framework:
565+
property_access: { enable_annotations: true }
566+
567+
.. code-block:: xml
568+
569+
<!-- app/config/config.xml -->
570+
<?xml version="1.0" encoding="UTF-8" ?>
571+
<container xmlns="http://symfony.com/schema/dic/services"
572+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
573+
xmlns:framework="http://symfony.com/schema/dic/symfony"
574+
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd
575+
http://symfony.com/schema/dic/symfony http://symfony.com/schema/dic/symfony/symfony-1.0.xsd">
576+
577+
<framework:config>
578+
<framework:property_access enable-annotations="true" />
579+
</framework:config>
580+
</container>
581+
582+
.. code-block:: php
583+
584+
// app/config/config.php
585+
$container->loadFromExtension('framework', array(
586+
'property_access' => array(
587+
'enable_annotations' => true,
588+
),
589+
));
590+
368591
Enable other Features
369-
~~~~~~~~~~~~~~~~~~~~~
592+
---------------------
370593

371594
The :class:`Symfony\\Component\\PropertyAccess\\PropertyAccessor` can be
372595
configured to enable extra features. To do that you could use the
@@ -397,5 +620,55 @@ Or you can pass parameters directly to the constructor (not the recommended way)
397620
// ...
398621
$accessor = new PropertyAccessor(true); // this enables handling of magic __call
399622

623+
If you need to enable metadata processing (see
624+
`Custom method calls and virtual properties in a class`_) you must instantiate
625+
a :class:`Symfony\\Componente\\PropertyAcces\\Mapping\\Factory\\MetadataFactoryInterface`
626+
and use the method `setMetadataFactory` on the
627+
:class:`Symfony\\Component\\PropertyAccess\\PropertyAccessorBuilder`. Bundled with
628+
the component you can find
629+
a `MetadataFactory` class that supports different kind of loaders (annotations,
630+
YAML and YML files) called :class:`
631+
Symfony\\Componente\\PropertyAcces\\Mapping\\Factory\\LazyLoadingMetadataFactory`.
632+
633+
Its constructor needs a :class:`
634+
Symfony\\Component\\PropertyAccess\\Mapping\\Loader\\LoaderInterface` which specifies
635+
the source of the metadata information. You can also use a PSR6 compliant cache
636+
as the second parameter passing a :class:`Psr\\Cache\\CacheItemPoolInterface`
637+
reference.
638+
639+
.. code-block:: php
640+
641+
use Doctrine\Common\Annotations\AnnotationReader;
642+
use Symfony\Component\PropertyAccess\Mapping\Factory\LazyLoadingMetadataFactory;
643+
use Symfony\Component\PropertyAccess\Mapping\Loader\AnnotationLoader;
644+
use Symfony\Component\PropertyAccess\Mapping\Loader\LoaderChain;
645+
use Symfony\Component\PropertyAccess\Mapping\Loader\XMLFileLoader;
646+
use Symfony\Component\PropertyAccess\Mapping\Loader\YamlFileLoader;
647+
648+
// ...
649+
650+
$accessorBuilder = PropertyAccess::createPropertyAccessorBuilder();
651+
652+
// Create annotation loader using Doctrine annotation reader
653+
$loader = new AnnotationLoader(new AnnotationReader());
654+
655+
// or read metadata from a XML file
656+
$loader = new XmlFileLoader('metadata.xml');
657+
658+
// or read metadata from a YAML file
659+
$loader = new YamlFileLoader('metadata.yml');
660+
661+
// or combine several loaders in one
662+
$loader = new LoaderChain(
663+
new AnnotationLoader(new AnnotationReader()),
664+
new XmlFileLoader('metadata.xml'),
665+
new YamlFileLoader('metadata.yml'),
666+
new YamlFileLoader('metadata2.yml')
667+
);
668+
669+
// Enable metadata loading
670+
$metadataFactory = new LazyLoadingMetadataFactory($loader);
671+
672+
$accessorBuilder->setMetadataFactory($metadataFactory);
400673
401674
.. _Packagist: https://packagist.org/packages/symfony/property-access

0 commit comments

Comments
 (0)