From 9d2ab9e348d8387029681fcf5cc084cdb19abfb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Sat, 21 Apr 2018 12:04:48 +0200 Subject: [PATCH] [PropertyInfo] Add an extractor to guess if a property is initializable --- .../FrameworkExtension.php | 3 ++ .../Resources/config/property_info.xml | 3 ++ .../Component/PropertyInfo/CHANGELOG.md | 5 +++ .../DependencyInjection/PropertyInfoPass.php | 7 ++++- .../Extractor/ReflectionExtractor.php | 31 ++++++++++++++++++- .../PropertyInfoCacheExtractor.php | 10 +++++- .../PropertyInfo/PropertyInfoExtractor.php | 23 ++++++++++---- ...ropertyInitializableExtractorInterface.php | 25 +++++++++++++++ .../AbstractPropertyInfoExtractorTest.php | 9 +++++- .../PropertyInfoPassTest.php | 6 ++-- .../Extractor/ReflectionExtractorTest.php | 23 ++++++++++++++ .../Tests/Fixtures/DummyExtractor.php | 11 ++++++- .../Tests/Fixtures/NotInstantiable.php | 22 +++++++++++++ .../Tests/Fixtures/NullExtractor.php | 11 ++++++- .../Tests/Fixtures/Php71Dummy.php | 19 ++++++++++++ .../Tests/PropertyInfoCacheExtractorTest.php | 6 ++++ 16 files changed, 200 insertions(+), 14 deletions(-) create mode 100644 src/Symfony/Component/PropertyInfo/PropertyInitializableExtractorInterface.php create mode 100644 src/Symfony/Component/PropertyInfo/Tests/Fixtures/NotInstantiable.php diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index 5ae98ada85026..af5e99d7f9f98 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -76,6 +76,7 @@ use Symfony\Component\PropertyAccess\PropertyAccessor; use Symfony\Component\PropertyInfo\PropertyAccessExtractorInterface; use Symfony\Component\PropertyInfo\PropertyDescriptionExtractorInterface; +use Symfony\Component\PropertyInfo\PropertyInitializableExtractorInterface; use Symfony\Component\PropertyInfo\PropertyListExtractorInterface; use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface; use Symfony\Component\Routing\Loader\AnnotationDirectoryLoader; @@ -347,6 +348,8 @@ public function load(array $configs, ContainerBuilder $container) ->addTag('property_info.description_extractor'); $container->registerForAutoconfiguration(PropertyAccessExtractorInterface::class) ->addTag('property_info.access_extractor'); + $container->registerForAutoconfiguration(PropertyInitializableExtractorInterface::class) + ->addTag('property_info.initializable_extractor'); $container->registerForAutoconfiguration(EncoderInterface::class) ->addTag('serializer.encoder'); $container->registerForAutoconfiguration(DecoderInterface::class) diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/property_info.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/property_info.xml index bcf2f33b10a3a..e8778f70822bf 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/property_info.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/property_info.xml @@ -12,18 +12,21 @@ + + + diff --git a/src/Symfony/Component/PropertyInfo/CHANGELOG.md b/src/Symfony/Component/PropertyInfo/CHANGELOG.md index 4e98c95e2781b..a81f3124a67c4 100644 --- a/src/Symfony/Component/PropertyInfo/CHANGELOG.md +++ b/src/Symfony/Component/PropertyInfo/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +4.2.0 +----- + +* added `PropertyInitializableExtractorInterface` to test if a property can be initialized through the constructor (implemented by `ReflectionExtractor`) + 3.3.0 ----- diff --git a/src/Symfony/Component/PropertyInfo/DependencyInjection/PropertyInfoPass.php b/src/Symfony/Component/PropertyInfo/DependencyInjection/PropertyInfoPass.php index dd547f562be0f..3ebf838cc42aa 100644 --- a/src/Symfony/Component/PropertyInfo/DependencyInjection/PropertyInfoPass.php +++ b/src/Symfony/Component/PropertyInfo/DependencyInjection/PropertyInfoPass.php @@ -30,14 +30,16 @@ class PropertyInfoPass implements CompilerPassInterface private $typeExtractorTag; private $descriptionExtractorTag; private $accessExtractorTag; + private $initializableExtractorTag; - public function __construct(string $propertyInfoService = 'property_info', string $listExtractorTag = 'property_info.list_extractor', string $typeExtractorTag = 'property_info.type_extractor', string $descriptionExtractorTag = 'property_info.description_extractor', string $accessExtractorTag = 'property_info.access_extractor') + public function __construct(string $propertyInfoService = 'property_info', string $listExtractorTag = 'property_info.list_extractor', string $typeExtractorTag = 'property_info.type_extractor', string $descriptionExtractorTag = 'property_info.description_extractor', string $accessExtractorTag = 'property_info.access_extractor', string $initializableExtractorTag = 'property_info.initializable_extractor') { $this->propertyInfoService = $propertyInfoService; $this->listExtractorTag = $listExtractorTag; $this->typeExtractorTag = $typeExtractorTag; $this->descriptionExtractorTag = $descriptionExtractorTag; $this->accessExtractorTag = $accessExtractorTag; + $this->initializableExtractorTag = $initializableExtractorTag; } /** @@ -62,5 +64,8 @@ public function process(ContainerBuilder $container) $accessExtractors = $this->findAndSortTaggedServices($this->accessExtractorTag, $container); $definition->replaceArgument(3, new IteratorArgument($accessExtractors)); + + $initializableExtractors = $this->findAndSortTaggedServices($this->initializableExtractorTag, $container); + $definition->replaceArgument(4, new IteratorArgument($initializableExtractors)); } } diff --git a/src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php b/src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php index da106645c3d37..b74a6115aae31 100644 --- a/src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php +++ b/src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php @@ -13,6 +13,7 @@ use Symfony\Component\Inflector\Inflector; use Symfony\Component\PropertyInfo\PropertyAccessExtractorInterface; +use Symfony\Component\PropertyInfo\PropertyInitializableExtractorInterface; use Symfony\Component\PropertyInfo\PropertyListExtractorInterface; use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface; use Symfony\Component\PropertyInfo\Type; @@ -24,7 +25,7 @@ * * @final */ -class ReflectionExtractor implements PropertyListExtractorInterface, PropertyTypeExtractorInterface, PropertyAccessExtractorInterface +class ReflectionExtractor implements PropertyListExtractorInterface, PropertyTypeExtractorInterface, PropertyAccessExtractorInterface, PropertyInitializableExtractorInterface { /** * @internal @@ -146,6 +147,34 @@ public function isWritable($class, $property, array $context = array()) return null !== $reflectionMethod; } + /** + * {@inheritdoc} + */ + public function isInitializable(string $class, string $property, array $context = array()): ?bool + { + try { + $reflectionClass = new \ReflectionClass($class); + } catch (\ReflectionException $e) { + return null; + } + + if (!$reflectionClass->isInstantiable()) { + return false; + } + + if ($constructor = $reflectionClass->getConstructor()) { + foreach ($constructor->getParameters() as $parameter) { + if ($property === $parameter->name) { + return true; + } + } + } elseif ($parentClass = $reflectionClass->getParentClass()) { + return $this->isInitializable($parentClass->getName(), $property); + } + + return false; + } + /** * @return Type[]|null */ diff --git a/src/Symfony/Component/PropertyInfo/PropertyInfoCacheExtractor.php b/src/Symfony/Component/PropertyInfo/PropertyInfoCacheExtractor.php index 90fd8a0fb8638..c708679484b84 100644 --- a/src/Symfony/Component/PropertyInfo/PropertyInfoCacheExtractor.php +++ b/src/Symfony/Component/PropertyInfo/PropertyInfoCacheExtractor.php @@ -20,7 +20,7 @@ * * @final */ -class PropertyInfoCacheExtractor implements PropertyInfoExtractorInterface +class PropertyInfoCacheExtractor implements PropertyInfoExtractorInterface, PropertyInitializableExtractorInterface { private $propertyInfoExtractor; private $cacheItemPool; @@ -80,6 +80,14 @@ public function getTypes($class, $property, array $context = array()) return $this->extract('getTypes', array($class, $property, $context)); } + /** + * {@inheritdoc} + */ + public function isInitializable(string $class, string $property, array $context = array()): ?bool + { + return $this->extract('isInitializable', array($class, $property, $context)); + } + /** * Retrieves the cached data if applicable or delegates to the decorated extractor. * diff --git a/src/Symfony/Component/PropertyInfo/PropertyInfoExtractor.php b/src/Symfony/Component/PropertyInfo/PropertyInfoExtractor.php index 4baa7440f995f..b6a7011d9c01e 100644 --- a/src/Symfony/Component/PropertyInfo/PropertyInfoExtractor.php +++ b/src/Symfony/Component/PropertyInfo/PropertyInfoExtractor.php @@ -18,25 +18,28 @@ * * @final */ -class PropertyInfoExtractor implements PropertyInfoExtractorInterface +class PropertyInfoExtractor implements PropertyInfoExtractorInterface, PropertyInitializableExtractorInterface { private $listExtractors; private $typeExtractors; private $descriptionExtractors; private $accessExtractors; + private $initializableExtractors; /** - * @param iterable|PropertyListExtractorInterface[] $listExtractors - * @param iterable|PropertyTypeExtractorInterface[] $typeExtractors - * @param iterable|PropertyDescriptionExtractorInterface[] $descriptionExtractors - * @param iterable|PropertyAccessExtractorInterface[] $accessExtractors + * @param iterable|PropertyListExtractorInterface[] $listExtractors + * @param iterable|PropertyTypeExtractorInterface[] $typeExtractors + * @param iterable|PropertyDescriptionExtractorInterface[] $descriptionExtractors + * @param iterable|PropertyAccessExtractorInterface[] $accessExtractors + * @param iterable|PropertyInitializableExtractorInterface[] $initializableExtractors */ - public function __construct(iterable $listExtractors = array(), iterable $typeExtractors = array(), iterable $descriptionExtractors = array(), iterable $accessExtractors = array()) + public function __construct(iterable $listExtractors = array(), iterable $typeExtractors = array(), iterable $descriptionExtractors = array(), iterable $accessExtractors = array(), iterable $initializableExtractors = array()) { $this->listExtractors = $listExtractors; $this->typeExtractors = $typeExtractors; $this->descriptionExtractors = $descriptionExtractors; $this->accessExtractors = $accessExtractors; + $this->initializableExtractors = $initializableExtractors; } /** @@ -87,6 +90,14 @@ public function isWritable($class, $property, array $context = array()) return $this->extract($this->accessExtractors, 'isWritable', array($class, $property, $context)); } + /** + * {@inheritdoc} + */ + public function isInitializable(string $class, string $property, array $context = array()): ?bool + { + return $this->extract($this->initializableExtractors, 'isInitializable', array($class, $property, $context)); + } + /** * Iterates over registered extractors and return the first value found. * diff --git a/src/Symfony/Component/PropertyInfo/PropertyInitializableExtractorInterface.php b/src/Symfony/Component/PropertyInfo/PropertyInitializableExtractorInterface.php new file mode 100644 index 0000000000000..c00ca35c2306f --- /dev/null +++ b/src/Symfony/Component/PropertyInfo/PropertyInitializableExtractorInterface.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyInfo; + +/** + * Guesses if the property can be initialized through the constructor. + * + * @author Kévin Dunglas + */ +interface PropertyInitializableExtractorInterface +{ + /** + * Is the property initializable? Returns true if a constructor's parameter matches the given property name. + */ + public function isInitializable(string $class, string $property, array $context = array()): ?bool; +} diff --git a/src/Symfony/Component/PropertyInfo/Tests/AbstractPropertyInfoExtractorTest.php b/src/Symfony/Component/PropertyInfo/Tests/AbstractPropertyInfoExtractorTest.php index 5b729c79e1e1f..6006798020984 100644 --- a/src/Symfony/Component/PropertyInfo/Tests/AbstractPropertyInfoExtractorTest.php +++ b/src/Symfony/Component/PropertyInfo/Tests/AbstractPropertyInfoExtractorTest.php @@ -13,6 +13,7 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\PropertyInfo\PropertyInfoExtractor; +use Symfony\Component\PropertyInfo\PropertyInitializableExtractorInterface; use Symfony\Component\PropertyInfo\Tests\Fixtures\DummyExtractor; use Symfony\Component\PropertyInfo\Tests\Fixtures\NullExtractor; use Symfony\Component\PropertyInfo\Type; @@ -30,7 +31,7 @@ class AbstractPropertyInfoExtractorTest extends TestCase protected function setUp() { $extractors = array(new NullExtractor(), new DummyExtractor()); - $this->propertyInfo = new PropertyInfoExtractor($extractors, $extractors, $extractors, $extractors); + $this->propertyInfo = new PropertyInfoExtractor($extractors, $extractors, $extractors, $extractors, $extractors); } public function testInstanceOf() @@ -39,6 +40,7 @@ public function testInstanceOf() $this->assertInstanceOf('Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface', $this->propertyInfo); $this->assertInstanceOf('Symfony\Component\PropertyInfo\PropertyDescriptionExtractorInterface', $this->propertyInfo); $this->assertInstanceOf('Symfony\Component\PropertyInfo\PropertyAccessExtractorInterface', $this->propertyInfo); + $this->assertInstanceOf(PropertyInitializableExtractorInterface::class, $this->propertyInfo); } public function testGetShortDescription() @@ -70,4 +72,9 @@ public function testGetProperties() { $this->assertEquals(array('a', 'b'), $this->propertyInfo->getProperties('Foo')); } + + public function testIsInitializable() + { + $this->assertTrue($this->propertyInfo->isInitializable('Foo', 'bar', array())); + } } diff --git a/src/Symfony/Component/PropertyInfo/Tests/DependencyInjection/PropertyInfoPassTest.php b/src/Symfony/Component/PropertyInfo/Tests/DependencyInjection/PropertyInfoPassTest.php index 031e34af97451..29feddce55f4d 100644 --- a/src/Symfony/Component/PropertyInfo/Tests/DependencyInjection/PropertyInfoPassTest.php +++ b/src/Symfony/Component/PropertyInfo/Tests/DependencyInjection/PropertyInfoPassTest.php @@ -26,7 +26,7 @@ public function testServicesAreOrderedAccordingToPriority($index, $tag) { $container = new ContainerBuilder(); - $definition = $container->register('property_info')->setArguments(array(null, null, null, null)); + $definition = $container->register('property_info')->setArguments(array(null, null, null, null, null)); $container->register('n2')->addTag($tag, array('priority' => 100)); $container->register('n1')->addTag($tag, array('priority' => 200)); $container->register('n3')->addTag($tag); @@ -49,6 +49,7 @@ public function provideTags() array(1, 'property_info.type_extractor'), array(2, 'property_info.description_extractor'), array(3, 'property_info.access_extractor'), + array(4, 'property_info.initializable_extractor'), ); } @@ -56,7 +57,7 @@ public function testReturningEmptyArrayWhenNoService() { $container = new ContainerBuilder(); $propertyInfoExtractorDefinition = $container->register('property_info') - ->setArguments(array(array(), array(), array(), array())); + ->setArguments(array(array(), array(), array(), array(), array())); $propertyInfoPass = new PropertyInfoPass(); $propertyInfoPass->process($container); @@ -65,5 +66,6 @@ public function testReturningEmptyArrayWhenNoService() $this->assertEquals(new IteratorArgument(array()), $propertyInfoExtractorDefinition->getArgument(1)); $this->assertEquals(new IteratorArgument(array()), $propertyInfoExtractorDefinition->getArgument(2)); $this->assertEquals(new IteratorArgument(array()), $propertyInfoExtractorDefinition->getArgument(3)); + $this->assertEquals(new IteratorArgument(array()), $propertyInfoExtractorDefinition->getArgument(4)); } } diff --git a/src/Symfony/Component/PropertyInfo/Tests/Extractor/ReflectionExtractorTest.php b/src/Symfony/Component/PropertyInfo/Tests/Extractor/ReflectionExtractorTest.php index 93b746cbfb154..762b0fed481cd 100644 --- a/src/Symfony/Component/PropertyInfo/Tests/Extractor/ReflectionExtractorTest.php +++ b/src/Symfony/Component/PropertyInfo/Tests/Extractor/ReflectionExtractorTest.php @@ -14,6 +14,9 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor; use Symfony\Component\PropertyInfo\Tests\Fixtures\AdderRemoverDummy; +use Symfony\Component\PropertyInfo\Tests\Fixtures\NotInstantiable; +use Symfony\Component\PropertyInfo\Tests\Fixtures\Php71Dummy; +use Symfony\Component\PropertyInfo\Tests\Fixtures\Php71DummyExtended2; use Symfony\Component\PropertyInfo\Type; /** @@ -270,4 +273,24 @@ public function testSingularize() $this->assertTrue($this->extractor->isWritable(AdderRemoverDummy::class, 'feet')); $this->assertEquals(array('analyses', 'feet'), $this->extractor->getProperties(AdderRemoverDummy::class)); } + + /** + * @dataProvider getInitializableProperties + */ + public function testIsInitializable(string $class, string $property, bool $expected) + { + $this->assertSame($expected, $this->extractor->isInitializable($class, $property)); + } + + public function getInitializableProperties(): array + { + return array( + array(Php71Dummy::class, 'string', true), + array(Php71Dummy::class, 'intPrivate', true), + array(Php71Dummy::class, 'notExist', false), + array(Php71DummyExtended2::class, 'intWithAccessor', true), + array(Php71DummyExtended2::class, 'intPrivate', false), + array(NotInstantiable::class, 'foo', false), + ); + } } diff --git a/src/Symfony/Component/PropertyInfo/Tests/Fixtures/DummyExtractor.php b/src/Symfony/Component/PropertyInfo/Tests/Fixtures/DummyExtractor.php index cfabcf2a90219..b09234b8a25c1 100644 --- a/src/Symfony/Component/PropertyInfo/Tests/Fixtures/DummyExtractor.php +++ b/src/Symfony/Component/PropertyInfo/Tests/Fixtures/DummyExtractor.php @@ -13,6 +13,7 @@ use Symfony\Component\PropertyInfo\PropertyAccessExtractorInterface; use Symfony\Component\PropertyInfo\PropertyDescriptionExtractorInterface; +use Symfony\Component\PropertyInfo\PropertyInitializableExtractorInterface; use Symfony\Component\PropertyInfo\PropertyListExtractorInterface; use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface; use Symfony\Component\PropertyInfo\Type; @@ -20,7 +21,7 @@ /** * @author Kévin Dunglas */ -class DummyExtractor implements PropertyListExtractorInterface, PropertyDescriptionExtractorInterface, PropertyTypeExtractorInterface, PropertyAccessExtractorInterface +class DummyExtractor implements PropertyListExtractorInterface, PropertyDescriptionExtractorInterface, PropertyTypeExtractorInterface, PropertyAccessExtractorInterface, PropertyInitializableExtractorInterface { /** * {@inheritdoc} @@ -69,4 +70,12 @@ public function getProperties($class, array $context = array()) { return array('a', 'b'); } + + /** + * {@inheritdoc} + */ + public function isInitializable(string $class, string $property, array $context = array()): ?bool + { + return true; + } } diff --git a/src/Symfony/Component/PropertyInfo/Tests/Fixtures/NotInstantiable.php b/src/Symfony/Component/PropertyInfo/Tests/Fixtures/NotInstantiable.php new file mode 100644 index 0000000000000..cda150162f633 --- /dev/null +++ b/src/Symfony/Component/PropertyInfo/Tests/Fixtures/NotInstantiable.php @@ -0,0 +1,22 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyInfo\Tests\Fixtures; + +/** + * @author Kévin Dunglas + */ +class NotInstantiable +{ + private function __construct(string $foo) + { + } +} diff --git a/src/Symfony/Component/PropertyInfo/Tests/Fixtures/NullExtractor.php b/src/Symfony/Component/PropertyInfo/Tests/Fixtures/NullExtractor.php index 1f263bfc3cd75..724f52530916b 100644 --- a/src/Symfony/Component/PropertyInfo/Tests/Fixtures/NullExtractor.php +++ b/src/Symfony/Component/PropertyInfo/Tests/Fixtures/NullExtractor.php @@ -13,6 +13,7 @@ use Symfony\Component\PropertyInfo\PropertyAccessExtractorInterface; use Symfony\Component\PropertyInfo\PropertyDescriptionExtractorInterface; +use Symfony\Component\PropertyInfo\PropertyInitializableExtractorInterface; use Symfony\Component\PropertyInfo\PropertyListExtractorInterface; use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface; @@ -21,7 +22,7 @@ * * @author Kévin Dunglas */ -class NullExtractor implements PropertyListExtractorInterface, PropertyDescriptionExtractorInterface, PropertyTypeExtractorInterface, PropertyAccessExtractorInterface +class NullExtractor implements PropertyListExtractorInterface, PropertyDescriptionExtractorInterface, PropertyTypeExtractorInterface, PropertyAccessExtractorInterface, PropertyInitializableExtractorInterface { /** * {@inheritdoc} @@ -76,6 +77,14 @@ public function getProperties($class, array $context = array()) $this->assertIsString($class); } + /** + * {@inheritdoc} + */ + public function isInitializable(string $class, string $property, array $context = array()): ?bool + { + return null; + } + private function assertIsString($string) { if (!\is_string($string)) { diff --git a/src/Symfony/Component/PropertyInfo/Tests/Fixtures/Php71Dummy.php b/src/Symfony/Component/PropertyInfo/Tests/Fixtures/Php71Dummy.php index e72d376c492fa..80012f968d70f 100644 --- a/src/Symfony/Component/PropertyInfo/Tests/Fixtures/Php71Dummy.php +++ b/src/Symfony/Component/PropertyInfo/Tests/Fixtures/Php71Dummy.php @@ -16,6 +16,10 @@ */ class Php71Dummy { + public function __construct(string $string, int $intPrivate) + { + } + public function getFoo(): ?array { } @@ -32,3 +36,18 @@ public function addBaz(string $baz) { } } + +class Php71DummyExtended extends Php71Dummy +{ +} + +class Php71DummyExtended2 extends Php71Dummy +{ + public function __construct(int $intWithAccessor) + { + } + + public function getIntWithAccessor() + { + } +} diff --git a/src/Symfony/Component/PropertyInfo/Tests/PropertyInfoCacheExtractorTest.php b/src/Symfony/Component/PropertyInfo/Tests/PropertyInfoCacheExtractorTest.php index d31a7bd51ddde..59dc3fb22be4c 100644 --- a/src/Symfony/Component/PropertyInfo/Tests/PropertyInfoCacheExtractorTest.php +++ b/src/Symfony/Component/PropertyInfo/Tests/PropertyInfoCacheExtractorTest.php @@ -61,4 +61,10 @@ public function testGetProperties() parent::testGetProperties(); parent::testGetProperties(); } + + public function testIsInitializable() + { + parent::testIsInitializable(); + parent::testIsInitializable(); + } } pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy