Skip to content

[PropertyAccess] Allow custom methods on property accesses #18016

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 18 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -628,6 +628,8 @@ private function addPropertyAccessSection(ArrayNodeDefinition $rootNode)
->addDefaultsIfNotSet()
->info('Property access configuration')
->children()
->scalarNode('cache')->end()
->booleanNode('enable_annotations')->defaultFalse()->end()
->booleanNode('magic_call')->defaultFalse()->end()
->booleanNode('throw_exception_on_invalid_index')->defaultFalse()->end()
->end()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,14 @@
use Symfony\Component\DependencyInjection\Exception\LogicException;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;
use Symfony\Component\PropertyAccess\Mapping\Loader\XmlFileLoader as PropertyAccessXmlFileLoader;
use Symfony\Component\Config\Resource\FileResource;
use Symfony\Component\Config\Resource\DirectoryResource;
use Symfony\Component\Finder\Finder;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\PropertyAccess\Mapping\Loader\AnnotationLoader;
use Symfony\Component\PropertyAccess\Mapping\Loader\YamlFileLoader;
use Symfony\Component\PropertyAccess\PropertyAccessor;
use Symfony\Component\Serializer\Mapping\Factory\CacheClassMetadataFactory;
use Symfony\Component\Serializer\Normalizer\DataUriNormalizer;
Expand Down Expand Up @@ -138,7 +141,7 @@ public function load(array $configs, ContainerBuilder $container)
}

$this->registerAnnotationsConfiguration($config['annotations'], $container, $loader);
$this->registerPropertyAccessConfiguration($config['property_access'], $container);
$this->registerPropertyAccessConfiguration($config['property_access'], $container, $loader);

if ($this->isConfigEnabled($container, $config['serializer'])) {
$this->registerSerializerConfiguration($config['serializer'], $container, $loader);
Expand Down Expand Up @@ -915,13 +918,75 @@ private function registerAnnotationsConfiguration(array $config, ContainerBuilde
}
}

private function registerPropertyAccessConfiguration(array $config, ContainerBuilder $container)
private function registerPropertyAccessConfiguration(array $config, ContainerBuilder $container, XmlFileLoader $loader)
{
$loader->load('property_access.xml');

$container
->getDefinition('property_accessor')
->replaceArgument(0, $config['magic_call'])
->replaceArgument(1, $config['throw_exception_on_invalid_index'])
;

$chainLoader = $container->getDefinition('property_access.mapping.chain_loader');

$serializerLoaders = array();
if (isset($config['enable_annotations']) && $config['enable_annotations']) {
$annotationLoader = new Definition(
AnnotationLoader::class,
array(new Reference('annotation_reader'))
);
$annotationLoader->setPublic(false);

$serializerLoaders[] = $annotationLoader;
}

$bundles = $container->getParameter('kernel.bundles');
foreach ($bundles as $bundle) {
$reflection = new \ReflectionClass($bundle);
$dirname = dirname($reflection->getFileName());

if (is_file($file = $dirname.'/Resources/config/property_access.xml')) {
$definition = new Definition(PropertyAccessXmlFileLoader::class, array(realpath($file)));
$definition->setPublic(false);

$serializerLoaders[] = $definition;
$container->addResource(new FileResource($file));
}

if (is_file($file = $dirname.'/Resources/config/property_access.yml')) {
$definition = new Definition(YamlFileLoader::class, array(realpath($file)));
$definition->setPublic(false);

$serializerLoaders[] = $definition;
$container->addResource(new FileResource($file));
}

if (is_dir($dir = $dirname.'/Resources/config/property_access')) {
foreach (Finder::create()->files()->in($dir)->name('*.xml') as $file) {
$definition = new Definition(PropertyAccessXmlFileLoader::class, array($file->getRealpath()));
$definition->setPublic(false);

$serializerLoaders[] = $definition;
}
foreach (Finder::create()->files()->in($dir)->name('*.yml') as $file) {
$definition = new Definition(YamlFileLoader::class, array($file->getRealpath()));
$definition->setPublic(false);

$serializerLoaders[] = $definition;
}

$container->addResource(new DirectoryResource($dir));
}
}

$chainLoader->replaceArgument(0, $serializerLoaders);

if (isset($config['cache']) && $config['cache']) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should be delayed until #17868 is merged.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, it makes sense

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure #17868 will be merged before the 3.1 feature freeze. Could we skip this and work on it after the new Cache component is ready?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should use the new system.cache infrastructure: http://symfony.com/blog/new-in-symfony-3-1-cache-component

$container->getDefinition('property_access.mapping.class_metadata_factory')->replaceArgument(
1, new Reference($config['cache'])
);
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,22 @@
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">

<services>
<!-- Loader -->
<service id="property_access.mapping.chain_loader" class="Symfony\Component\PropertyAccess\Mapping\Loader\LoaderChain" public="false">
<argument type="collection" />
</service>

<!-- Class Metadata Factory -->
<service id="property_access.mapping.class_metadata_factory" class="Symfony\Component\PropertyAccess\Mapping\Factory\LazyLoadingMetadataFactory" public="false">
<argument type="service" id="property_access.mapping.chain_loader" />
<argument>null</argument>
</service>

<service id="property_accessor" class="Symfony\Component\PropertyAccess\PropertyAccessor" >
<argument /> <!-- magicCall, set by the extension -->
<argument /> <!-- throwExceptionOnInvalidIndex, set by the extension -->
<argument type="service" id="cache.property_access" on-invalid="ignore" />
<argument type="service" id="property_access.mapping.class_metadata_factory" on-invalid="ignore" />
</service>
</services>
</container>
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,9 @@

<xsd:complexType name="property_access">
<xsd:attribute name="magic-call" type="xsd:boolean" />
<xsd:attribute name="cache" type="xsd:string" />
<xsd:attribute name="throw-exception-on-invalid-index" type="xsd:boolean" />
<xsd:attribute name="enable-annotations" type="xsd:boolean" />
</xsd:complexType>

<xsd:complexType name="serializer">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,7 @@ protected static function getBundleDefaultConfig()
'property_access' => array(
'magic_call' => false,
'throw_exception_on_invalid_index' => false,
'enable_annotations' => false,
),
'property_info' => array(
'enabled' => false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,11 @@
'debug' => true,
'file_cache_dir' => '%kernel.cache_dir%/annotations',
),
'property_access' => array(
'magic_call' => false,
'throw_exception_on_invalid_index' => false,
'enable_annotations' => true,
),
'serializer' => array(
'enabled' => true,
'enable_annotations' => true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@
'property_access' => array(
'magic_call' => true,
'throw_exception_on_invalid_index' => true,
'enable_annotations' => false,
),
));
Original file line number Diff line number Diff line change
Expand Up @@ -41,5 +41,6 @@
<framework:validation enabled="true" cache="validator.mapping.cache.doctrine.apc" />
<framework:annotations cache="file" debug="true" file-cache-dir="%kernel.cache_dir%/annotations" />
<framework:serializer enabled="true" enable-annotations="true" name-converter="serializer.name_converter.camel_case_to_snake_case" />
<framework:property-access cache="property_access.mapping.cache.doctrine.apc" enable-annotations="true" magic-call="false" throw-exception-on-invalid-index="false" />
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be removed and use the new cache component instead.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right.

</framework:config>
</container>
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ framework:
enabled: true
enable_annotations: true
name_converter: serializer.name_converter.camel_case_to_snake_case
property_access:
enable_annotations: true
magic_call: false
throw_exception_on_invalid_index: false
ide: file%%link%%format
request:
formats:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ framework:
property_access:
magic_call: true
throw_exception_on_invalid_index: true
enable_annotations: false
51 changes: 51 additions & 0 deletions src/Symfony/Component/PropertyAccess/Annotation/Property.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\PropertyAccess\Annotation;

/**
* Property accessor configuration annotation.
*
* @Annotation
* @Target({"PROPERTY"})
*
* @author Luis Ramón López <lrlopez@gmail.com>
*/
class Property
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This annotation duplicates the other annotations. I don't think we should add multiple ways to add the same thing:

  • more code to maintain
  • harder to improve performance
  • harder to introduce changes consistently
  • harder to understand for our users when to use which

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are also some advantages of using property annotations:

  • the annotation is next to the property declaration, which is very convenient when looking for it. On the other hand, method annotations are spread out in the class definition (properties are usually placed on the top of the class and method implementations at the end)
  • you can override several methods with just one annotation

Property annotations are implemented with just 19 lines of code (not taking into account the empty annotation class). In fact, all new annotations are loaded from the same method AnnotationLoader->loadClassMetadata(), so the code will be easy to maintain and optimize as changes would be centralized.

About being harder to understand for newcomers, we could just give them some initial guidelines (i.e. method annotations for virtual properties and property annotations for everything else) and let them use whatever they want, just like the YAML vs XML vs Annotation debate.

Anyway, if you feel that only method annotations must be kept, I'll do that, of course!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with @webmozart, it's weird to have different way to do the same thing. What about having only a @Property annotation with an optional name attribute in case you use it on a method?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dunglas @webmozart 👍 for having only one annotation. @Property looks weird on a method, maybe @Accessor ?

{
/**
* Custom setter method for the property.
*
* @var string
*/
public $setter;

/**
* Custom getter method for the property.
*
* @var string
*/
public $getter;

/**
* Custom adder method for the property.
*
* @var string
*/
public $adder;

/**
* Custom remover method for the property.
*
* @var string
*/
public $remover;
}
30 changes: 30 additions & 0 deletions src/Symfony/Component/PropertyAccess/Annotation/PropertyAdder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\PropertyAccess\Annotation;

/**
* Property accessor adder configuration annotation.
*
* @Annotation
* @Target({"METHOD"})
*
* @author Luis Ramón López <lrlopez@gmail.com>
*/
class PropertyAdder
{
/**
* Associates this method to the adder of this property.
*
* @var string
*/
public $property;
}
30 changes: 30 additions & 0 deletions src/Symfony/Component/PropertyAccess/Annotation/PropertyGetter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\PropertyAccess\Annotation;

/**
* Property accessor getter configuration annotation.
*
* @Annotation
* @Target({"METHOD"})
*
* @author Luis Ramón López <lrlopez@gmail.com>
*/
class PropertyGetter
{
/**
* Associates this method to the getter of this property.
*
* @var string
*/
public $property;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\PropertyAccess\Annotation;

/**
* Property accessor remover configuration annotation.
*
* @Annotation
* @Target({"METHOD"})
*
* @author Luis Ramón López <lrlopez@gmail.com>
*/
class PropertyRemover
{
/**
* Associates this method to the remover of this property.
*
* @var string
*/
public $property;
}
30 changes: 30 additions & 0 deletions src/Symfony/Component/PropertyAccess/Annotation/PropertySetter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\PropertyAccess\Annotation;

/**
* Property accessor setter configuration annotation.
*
* @Annotation
* @Target({"METHOD"})
*
* @author Luis Ramón López <lrlopez@gmail.com>
*/
class PropertySetter
{
/**
* Associates this method to the setter of this property.
*
* @var string
*/
public $property;
}
4 changes: 4 additions & 0 deletions src/Symfony/Component/PropertyAccess/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
CHANGELOG
=========

3.2.0
------
* added custom method calling for properties.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is unclear.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I'll add a more thoughtful explanation


2.7.0
------

Expand Down
Loading