Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit aa4cc90

Browse filesBrowse files
dunglasfabpot
authored andcommitted
[PropertyAccess] Port of the performance optimization from 2.3
1 parent 399b1d5 commit aa4cc90
Copy full SHA for aa4cc90

File tree

1 file changed

+209
-77
lines changed
Filter options

1 file changed

+209
-77
lines changed

‎src/Symfony/Component/PropertyAccess/PropertyAccessor.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/PropertyAccess/PropertyAccessor.php
+209-77Lines changed: 209 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,24 @@
2020
* Default implementation of {@link PropertyAccessorInterface}.
2121
*
2222
* @author Bernhard Schussek <bschussek@gmail.com>
23+
* @author Kévin Dunglas <dunglas@gmail.com>
2324
*/
2425
class PropertyAccessor implements PropertyAccessorInterface
2526
{
2627
const VALUE = 0;
2728
const IS_REF = 1;
2829
const IS_REF_CHAINED = 2;
30+
const ACCESS_HAS_PROPERTY = 0;
31+
const ACCESS_TYPE = 1;
32+
const ACCESS_NAME = 2;
33+
const ACCESS_REF = 3;
34+
const ACCESS_ADDER = 4;
35+
const ACCESS_REMOVER = 5;
36+
const ACCESS_TYPE_METHOD = 0;
37+
const ACCESS_TYPE_PROPERTY = 1;
38+
const ACCESS_TYPE_MAGIC = 2;
39+
const ACCESS_TYPE_ADDER_AND_REMOVER = 3;
40+
const ACCESS_TYPE_NOT_FOUND = 4;
2941

3042
/**
3143
* @var bool
@@ -37,6 +49,16 @@ class PropertyAccessor implements PropertyAccessorInterface
3749
*/
3850
private $ignoreInvalidIndices;
3951

52+
/**
53+
* @var array
54+
*/
55+
private $readPropertyCache = array();
56+
57+
/**
58+
* @var array
59+
*/
60+
private $writePropertyCache = array();
61+
4062
/**
4163
* Should not be used by application code. Use
4264
* {@link PropertyAccess::createPropertyAccessor()} instead.
@@ -78,7 +100,7 @@ public function setValue(&$objectOrArray, $propertyPath, $value)
78100
self::IS_REF => true,
79101
self::IS_REF_CHAINED => true,
80102
));
81-
103+
82104
$propertyMaxIndex = count($propertyValues) - 1;
83105

84106
for ($i = $propertyMaxIndex; $i >= 0; --$i) {
@@ -330,51 +352,31 @@ private function &readProperty(&$object, $property)
330352
throw new NoSuchPropertyException(sprintf('Cannot read property "%s" from an array. Maybe you intended to write the property path as "[%s]" instead.', $property, $property));
331353
}
332354

333-
$camelized = $this->camelize($property);
334-
$reflClass = new \ReflectionClass($object);
335-
$getter = 'get'.$camelized;
336-
$getsetter = lcfirst($camelized); // jQuery style, e.g. read: last(), write: last($item)
337-
$isser = 'is'.$camelized;
338-
$hasser = 'has'.$camelized;
339-
$classHasProperty = $reflClass->hasProperty($property);
355+
$access = $this->getReadAccessInfo($object, $property);
340356

341-
if ($reflClass->hasMethod($getter) && $reflClass->getMethod($getter)->isPublic()) {
342-
$result[self::VALUE] = $object->$getter();
343-
} elseif ($this->isMethodAccessible($reflClass, $getsetter, 0)) {
344-
$result[self::VALUE] = $object->$getsetter();
345-
} elseif ($reflClass->hasMethod($isser) && $reflClass->getMethod($isser)->isPublic()) {
346-
$result[self::VALUE] = $object->$isser();
347-
} elseif ($reflClass->hasMethod($hasser) && $reflClass->getMethod($hasser)->isPublic()) {
348-
$result[self::VALUE] = $object->$hasser();
349-
} elseif ($classHasProperty && $reflClass->getProperty($property)->isPublic()) {
350-
$result[self::VALUE] = &$object->$property;
351-
$result[self::IS_REF] = true;
352-
} elseif ($reflClass->hasMethod('__get') && $reflClass->getMethod('__get')->isPublic()) {
353-
$result[self::VALUE] = $object->$property;
354-
} elseif (!$classHasProperty && property_exists($object, $property)) {
357+
if (self::ACCESS_TYPE_METHOD === $access[self::ACCESS_TYPE]) {
358+
$result[self::VALUE] = $object->{$access[self::ACCESS_NAME]}();
359+
} elseif (self::ACCESS_TYPE_PROPERTY === $access[self::ACCESS_TYPE]) {
360+
if ($access[self::ACCESS_REF]) {
361+
$result[self::VALUE] = &$object->{$access[self::ACCESS_NAME]};
362+
$result[self::IS_REF] = true;
363+
} else {
364+
$result[self::VALUE] = $object->{$access[self::ACCESS_NAME]};
365+
}
366+
} elseif (!$access[self::ACCESS_HAS_PROPERTY] && property_exists($object, $property)) {
355367
// Needed to support \stdClass instances. We need to explicitly
356368
// exclude $classHasProperty, otherwise if in the previous clause
357369
// a *protected* property was found on the class, property_exists()
358370
// returns true, consequently the following line will result in a
359371
// fatal error.
372+
360373
$result[self::VALUE] = &$object->$property;
361374
$result[self::IS_REF] = true;
362-
} elseif ($this->magicCall && $reflClass->hasMethod('__call') && $reflClass->getMethod('__call')->isPublic()) {
375+
} elseif (self::ACCESS_TYPE_MAGIC === $access[self::ACCESS_TYPE]) {
363376
// we call the getter and hope the __call do the job
364-
$result[self::VALUE] = $object->$getter();
377+
$result[self::VALUE] = $object->{$access[self::ACCESS_NAME]}();
365378
} else {
366-
$methods = array($getter, $getsetter, $isser, $hasser, '__get');
367-
if ($this->magicCall) {
368-
$methods[] = '__call';
369-
}
370-
371-
throw new NoSuchPropertyException(sprintf(
372-
'Neither the property "%s" nor one of the methods "%s()" '.
373-
'exist and have public access in class "%s".',
374-
$property,
375-
implode('()", "', $methods),
376-
$reflClass->name
377-
));
379+
throw new NoSuchPropertyException($access[self::ACCESS_NAME]);
378380
}
379381

380382
// Objects are always passed around by reference
@@ -385,6 +387,81 @@ private function &readProperty(&$object, $property)
385387
return $result;
386388
}
387389

390+
/**
391+
* Guesses how to read the property value.
392+
*
393+
* @param string $object
394+
* @param string $property
395+
*
396+
* @return array
397+
*/
398+
private function getReadAccessInfo($object, $property)
399+
{
400+
$key = get_class($object).'::'.$property;
401+
402+
if (isset($this->readPropertyCache[$key])) {
403+
$access = $this->readPropertyCache[$key];
404+
} else {
405+
$access = array();
406+
407+
$reflClass = new \ReflectionClass($object);
408+
$access[self::ACCESS_HAS_PROPERTY] = $reflClass->hasProperty($property);
409+
$camelProp = $this->camelize($property);
410+
$getter = 'get'.$camelProp;
411+
$getsetter = lcfirst($camelProp); // jQuery style, e.g. read: last(), write: last($item)
412+
$isser = 'is'.$camelProp;
413+
$hasser = 'has'.$camelProp;
414+
$classHasProperty = $reflClass->hasProperty($property);
415+
416+
if ($reflClass->hasMethod($getter) && $reflClass->getMethod($getter)->isPublic()) {
417+
$access[self::ACCESS_TYPE] = self::ACCESS_TYPE_METHOD;
418+
$access[self::ACCESS_NAME] = $getter;
419+
} elseif ($reflClass->hasMethod($getsetter) && $reflClass->getMethod($getsetter)->isPublic()) {
420+
$access[self::ACCESS_TYPE] = self::ACCESS_TYPE_METHOD;
421+
$access[self::ACCESS_NAME] = $getsetter;
422+
} elseif ($reflClass->hasMethod($isser) && $reflClass->getMethod($isser)->isPublic()) {
423+
$access[self::ACCESS_TYPE] = self::ACCESS_TYPE_METHOD;
424+
$access[self::ACCESS_NAME] = $isser;
425+
} elseif ($reflClass->hasMethod($hasser) && $reflClass->getMethod($hasser)->isPublic()) {
426+
$access[self::ACCESS_TYPE] = self::ACCESS_TYPE_METHOD;
427+
$access[self::ACCESS_NAME] = $hasser;
428+
} elseif ($reflClass->hasMethod('__get') && $reflClass->getMethod('__get')->isPublic()) {
429+
$access[self::ACCESS_TYPE] = self::ACCESS_TYPE_PROPERTY;
430+
$access[self::ACCESS_NAME] = $property;
431+
$access[self::ACCESS_REF] = false;
432+
} elseif ($classHasProperty && $reflClass->getProperty($property)->isPublic()) {
433+
$access[self::ACCESS_TYPE] = self::ACCESS_TYPE_PROPERTY;
434+
$access[self::ACCESS_NAME] = $property;
435+
$access[self::ACCESS_REF] = true;
436+
437+
$result[self::VALUE] = &$object->$property;
438+
$result[self::IS_REF] = true;
439+
} elseif ($this->magicCall && $reflClass->hasMethod('__call') && $reflClass->getMethod('__call')->isPublic()) {
440+
// we call the getter and hope the __call do the job
441+
$access[self::ACCESS_TYPE] = self::ACCESS_TYPE_MAGIC;
442+
$access[self::ACCESS_NAME] = $getter;
443+
} else {
444+
$methods = array($getter, $getsetter, $isser, $hasser, '__get');
445+
if ($this->magicCall) {
446+
$methods[] = '__call';
447+
}
448+
449+
$access[self::ACCESS_TYPE] = self::ACCESS_TYPE_NOT_FOUND;
450+
$access[self::ACCESS_NAME] = sprintf(
451+
'Neither the property "%s" nor one of the methods "%s()" '.
452+
'exist and have public access in class "%s".',
453+
$property,
454+
implode('()", "', $methods),
455+
$reflClass->name
456+
);
457+
}
458+
459+
$this->readPropertyCache[$key] = $access;
460+
}
461+
462+
return $access;
463+
}
464+
388465
/**
389466
* Sets the value of an index in a given array-accessible value.
390467
*
@@ -419,55 +496,26 @@ private function writeProperty(&$object, $property, $value)
419496
throw new NoSuchPropertyException(sprintf('Cannot write property "%s" to an array. Maybe you should write the property path as "[%s]" instead?', $property, $property));
420497
}
421498

422-
$reflClass = new \ReflectionClass($object);
423-
$camelized = $this->camelize($property);
424-
$singulars = (array) StringUtil::singularify($camelized);
425-
426-
if (is_array($value) || $value instanceof \Traversable) {
427-
$methods = $this->findAdderAndRemover($reflClass, $singulars);
428-
429-
// Use addXxx() and removeXxx() to write the collection
430-
if (null !== $methods) {
431-
$this->writeCollection($object, $property, $value, $methods[0], $methods[1]);
499+
$access = $this->getWriteAccessInfo($object, $property, $value);
432500

433-
return;
434-
}
435-
}
436-
437-
$setter = 'set'.$camelized;
438-
$getsetter = lcfirst($camelized); // jQuery style, e.g. read: last(), write: last($item)
439-
$classHasProperty = $reflClass->hasProperty($property);
440-
441-
if ($this->isMethodAccessible($reflClass, $setter, 1)) {
442-
$object->$setter($value);
443-
} elseif ($this->isMethodAccessible($reflClass, $getsetter, 1)) {
444-
$object->$getsetter($value);
445-
} elseif ($classHasProperty && $reflClass->getProperty($property)->isPublic()) {
446-
$object->$property = $value;
447-
} elseif ($this->isMethodAccessible($reflClass, '__set', 2)) {
448-
$object->$property = $value;
449-
} elseif (!$classHasProperty && property_exists($object, $property)) {
501+
if (self::ACCESS_TYPE_METHOD === $access[self::ACCESS_TYPE]) {
502+
$object->{$access[self::ACCESS_NAME]}($value);
503+
} elseif (self::ACCESS_TYPE_PROPERTY === $access[self::ACCESS_TYPE]) {
504+
$object->{$access[self::ACCESS_NAME]} = $value;
505+
} elseif (self::ACCESS_TYPE_ADDER_AND_REMOVER === $access[self::ACCESS_TYPE]) {
506+
$this->writeCollection($object, $property, $value, $access[self::ACCESS_ADDER], $access[self::ACCESS_REMOVER]);
507+
} elseif (!$access[self::ACCESS_HAS_PROPERTY] && property_exists($object, $property)) {
450508
// Needed to support \stdClass instances. We need to explicitly
451509
// exclude $classHasProperty, otherwise if in the previous clause
452510
// a *protected* property was found on the class, property_exists()
453511
// returns true, consequently the following line will result in a
454512
// fatal error.
513+
455514
$object->$property = $value;
456-
} elseif ($this->magicCall && $this->isMethodAccessible($reflClass, '__call', 2)) {
457-
// we call the getter and hope the __call do the job
458-
$object->$setter($value);
515+
} elseif (self::ACCESS_TYPE_MAGIC === $access[self::ACCESS_TYPE]) {
516+
$object->{$access[self::ACCESS_NAME]}($value);
459517
} else {
460-
throw new NoSuchPropertyException(sprintf(
461-
'Neither the property "%s" nor one of the methods %s"%s()", "%s()", '.
462-
'"__set()" or "__call()" exist and have public access in class "%s".',
463-
$property,
464-
implode('', array_map(function ($singular) {
465-
return '"add'.$singular.'()"/"remove'.$singular.'()", ';
466-
}, $singulars)),
467-
$setter,
468-
$getsetter,
469-
$reflClass->name
470-
));
518+
throw new NoSuchPropertyException($access[self::ACCESS_NAME]);
471519
}
472520
}
473521

@@ -519,6 +567,90 @@ private function writeCollection($object, $property, $collection, $addMethod, $r
519567
}
520568
}
521569

570+
/**
571+
* Guesses how to write the property value.
572+
*
573+
* @param string $object
574+
* @param string $property
575+
* @param mixed $value
576+
*
577+
* @return array
578+
*/
579+
private function getWriteAccessInfo($object, $property, $value)
580+
{
581+
$key = get_class($object).'::'.$property;
582+
$guessedAdders = '';
583+
584+
if (isset($this->writePropertyCache[$key])) {
585+
$access = $this->writePropertyCache[$key];
586+
} else {
587+
$access = array();
588+
589+
$reflClass = new \ReflectionClass($object);
590+
$access[self::ACCESS_HAS_PROPERTY] = $reflClass->hasProperty($property);
591+
$camelized = $this->camelize($property);
592+
$singulars = (array) StringUtil::singularify($camelized);
593+
594+
if (is_array($value) || $value instanceof \Traversable) {
595+
$methods = $this->findAdderAndRemover($reflClass, $singulars);
596+
597+
if (null === $methods) {
598+
// It is sufficient to include only the adders in the error
599+
// message. If the user implements the adder but not the remover,
600+
// an exception will be thrown in findAdderAndRemover() that
601+
// the remover has to be implemented as well.
602+
$guessedAdders = '"add'.implode('()", "add', $singulars).'()", ';
603+
} else {
604+
$access[self::ACCESS_TYPE] = self::ACCESS_TYPE_ADDER_AND_REMOVER;
605+
$access[self::ACCESS_ADDER] = $methods[0];
606+
$access[self::ACCESS_REMOVER] = $methods[1];
607+
}
608+
}
609+
610+
if (!isset($access[self::ACCESS_TYPE])) {
611+
$setter = 'set'.$this->camelize($property);
612+
$getsetter = lcfirst($camelized); // jQuery style, e.g. read: last(), write: last($item)
613+
614+
$classHasProperty = $reflClass->hasProperty($property);
615+
616+
if ($this->isMethodAccessible($reflClass, $setter, 1)) {
617+
$access[self::ACCESS_TYPE] = self::ACCESS_TYPE_METHOD;
618+
$access[self::ACCESS_NAME] = $setter;
619+
} elseif ($this->isMethodAccessible($reflClass, $getsetter, 1)) {
620+
$access[self::ACCESS_TYPE] = self::ACCESS_TYPE_METHOD;
621+
$access[self::ACCESS_NAME] = $getsetter;
622+
} elseif ($this->isMethodAccessible($reflClass, '__set', 2)) {
623+
$access[self::ACCESS_TYPE] = self::ACCESS_TYPE_PROPERTY;
624+
$access[self::ACCESS_NAME] = $property;
625+
} elseif ($classHasProperty && $reflClass->getProperty($property)->isPublic()) {
626+
$access[self::ACCESS_TYPE] = self::ACCESS_TYPE_PROPERTY;
627+
$access[self::ACCESS_NAME] = $property;
628+
} elseif ($this->magicCall && $this->isMethodAccessible($reflClass, '__call', 2)) {
629+
// we call the getter and hope the __call do the job
630+
$access[self::ACCESS_TYPE] = self::ACCESS_TYPE_MAGIC;
631+
$access[self::ACCESS_NAME] = $setter;
632+
} else {
633+
$access[self::ACCESS_TYPE] = self::ACCESS_TYPE_NOT_FOUND;
634+
$access[self::ACCESS_NAME] = sprintf(
635+
'Neither the property "%s" nor one of the methods %s"%s()", "%s()", '.
636+
'"__set()" or "__call()" exist and have public access in class "%s".',
637+
$property,
638+
implode('', array_map(function ($singular) {
639+
return '"add'.$singular.'()"/"remove'.$singular.'()", ';
640+
}, $singulars)),
641+
$setter,
642+
$getsetter,
643+
$reflClass->name
644+
);
645+
}
646+
}
647+
648+
$this->writePropertyCache[$key] = $access;
649+
}
650+
651+
return $access;
652+
}
653+
522654
/**
523655
* Returns whether a property is writable in the given object.
524656
*

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.