├── .github
├── FUNDING.yml
└── workflows
│ ├── continuous-integration.yml
│ └── release-on-milestone-closed.yml
├── .gitignore
├── renovate.json
├── infection.json.dist
├── phpunit.xml.dist
├── psalm.xml
├── src
└── LazyProperty
│ ├── Exception
│ ├── Exception.php
│ ├── InvalidLazyProperty.php
│ ├── MissingLazyPropertyGetter.php
│ └── InvalidAccess.php
│ ├── Util
│ └── AccessScopeChecker.php
│ └── LazyPropertiesTrait.php
├── tests
├── LazyPropertyTestAsset
│ ├── InheritedPropertiesClass.php
│ ├── AClass.php
│ ├── BClass.php
│ ├── LazyGetterClass.php
│ ├── ParentClass.php
│ └── MixedPropertiesClass.php
└── LazyPropertyTest
│ ├── Exception
│ ├── InvalidLazyPropertyTest.php
│ ├── InvalidAccessTest.php
│ └── MissingLazyPropertyGetterTest.php
│ ├── Util
│ └── AccessScopeCheckerTest.php
│ └── LazyPropertiesTraitTest.php
├── phpcs.xml.dist
├── LICENSE
├── CONTRIBUTING.md
├── composer.json
└── README.md
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: [Ocramius]
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | vendor
2 | phpunit.xml
3 | .phpunit.result.cache
4 | phpmd.xml
5 | phpdox.xml
6 | infection-log.txt
7 | infection.json
8 |
--------------------------------------------------------------------------------
/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3 | "extends": [
4 | "local>Ocramius/.github:renovate-config"
5 | ]
6 | }
7 |
--------------------------------------------------------------------------------
/infection.json.dist:
--------------------------------------------------------------------------------
1 | {
2 | "source": {
3 | "directories": [
4 | "src"
5 | ]
6 | },
7 | "logs": {
8 | "text": "php://stderr",
9 | "stryker": {
10 | "report": "/^\\d\\.\\d\\.x$/"
11 | }
12 | },
13 | "minMsi": 84,
14 | "minCoveredMsi": 84
15 | }
16 |
--------------------------------------------------------------------------------
/phpunit.xml.dist:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 | ./tests/LazyPropertyTest
9 |
10 |
11 |
12 | ./src
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/psalm.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/src/LazyProperty/Exception/Exception.php:
--------------------------------------------------------------------------------
1 | initLazyProperties($properties);
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/tests/LazyPropertyTestAsset/AClass.php:
--------------------------------------------------------------------------------
1 | initLazyProperties(['private']);
23 | }
24 |
25 | private function getPrivate(): string
26 | {
27 | return $this->private ?: $this->private = self::class;
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/tests/LazyPropertyTestAsset/BClass.php:
--------------------------------------------------------------------------------
1 | initLazyProperties(['private']);
23 | }
24 |
25 | private function getPrivate(): string
26 | {
27 | return $this->private ?: $this->private = self::class;
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/phpcs.xml.dist:
--------------------------------------------------------------------------------
1 |
2 |
4 | This project follows doctrine/coding-standard
5 |
6 | src
7 | tests
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/src/LazyProperty/Exception/InvalidLazyProperty.php:
--------------------------------------------------------------------------------
1 | assertStringMatchesFormat(
21 | 'The requested lazy property "foo" is not defined in "stdClass#%s"',
22 | InvalidLazyProperty::nonExistingLazyProperty(new stdClass(), 'foo')->getMessage(),
23 | );
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/LazyProperty/Exception/MissingLazyPropertyGetter.php:
--------------------------------------------------------------------------------
1 | assertStringMatchesFormat(
21 | 'The requested lazy property "foo" of "stdClass#%s" is not accessible from the context of in "'
22 | . self::class . '"',
23 | InvalidAccess::invalidContext($this, new stdClass(), 'foo')->getMessage(),
24 | );
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/tests/LazyPropertyTestAsset/LazyGetterClass.php:
--------------------------------------------------------------------------------
1 | initLazyProperties($properties);
24 | }
25 |
26 | public function getProperty(): string
27 | {
28 | if ($this->property === null) {
29 | return $this->property = 'property';
30 | }
31 |
32 | return $this->property;
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/tests/LazyPropertyTest/Exception/MissingLazyPropertyGetterTest.php:
--------------------------------------------------------------------------------
1 | assertStringMatchesFormat(
21 | 'The getter "getFoo" for lazy property "foo" is not defined in "stdClass#%s"',
22 | MissingLazyPropertyGetter::fromGetter(new stdClass(), 'getFoo', 'foo')->getMessage(),
23 | );
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/LazyProperty/Exception/InvalidAccess.php:
--------------------------------------------------------------------------------
1 | $propertyName;
22 | }
23 |
24 | private function getPrivate1(): string
25 | {
26 | return $this->private1 = 'private1';
27 | }
28 |
29 | private function getPrivate2(): string
30 | {
31 | return $this->private2 = 'private';
32 | }
33 |
34 | protected function getProtected1(): string
35 | {
36 | return $this->protected1 = 'protected1';
37 | }
38 |
39 | protected function getProtected2(): string
40 | {
41 | return $this->protected2 = 'protected2';
42 | }
43 |
44 | public function getPublic1(): string
45 | {
46 | return $this->public1 = 'public1';
47 | }
48 |
49 | public function getPublic2(): string
50 | {
51 | return $this->public2 = 'public2';
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | * The project will follow strict [object calisthenics](http://www.slideshare.net/guilhermeblanco/object-calisthenics-applied-to-php)
4 | * Any contribution must provide tests for additional introduced conditions
5 | * Any un-confirmed issue needs a failing test case before being accepted
6 | * Pull requests must be sent from a new hotfix/feature branch, not from `master`.
7 |
8 | ## Installation
9 |
10 | To install the project and run the tests, you need to clone it first:
11 |
12 | ```sh
13 | $ git clone git://github.com/Ocramius/LazyProperty.git
14 | ```
15 |
16 | You will then need to run a composer installation:
17 |
18 | ```sh
19 | $ cd LazyProperty
20 | $ curl -s https://getcomposer.org/installer | php
21 | $ php composer.phar update
22 | ```
23 |
24 | ## Testing
25 |
26 | The PHPUnit version to be used is the one installed as a dev- dependency via composer:
27 |
28 | ```sh
29 | $ ./vendor/bin/phpunit
30 | ```
31 |
32 | Accepted coverage for new contributions is 80%. Any contribution not satisfying this requirement
33 | won't be merged.
34 |
35 | ## Code Style
36 |
37 | The project follows the [Doctrine Coding Standard](https://github.com/doctrine/coding-standard). A configuration for php_codesniffer is shipped.
38 | Please execute the checker command and make sure you meet the requirements:
39 |
40 | ```sh
41 | $ ./vendor/bin/phpcs
42 | ```
43 |
44 | On Mac, you might need to add `--parallel=1` due to a known [bug](https://github.com/squizlabs/PHP_CodeSniffer/issues/2304).
45 |
46 | Disabling specific rules for parts of files where it's clearly a false positive is acceptable in some cases.
47 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ocramius/lazy-property",
3 | "description": "A library that provides lazy instantiation logic for object properties",
4 | "type": "library",
5 | "license": "MIT",
6 | "homepage": "https://github.com/Ocramius/LazyProperty",
7 | "minimum-stability": "stable",
8 | "keywords": [
9 | "lazy",
10 | "lazy loading",
11 | "lazy instantiation",
12 | "utility"
13 | ],
14 | "authors": [
15 | {
16 | "name": "Marco Pivetta",
17 | "email": "ocramius@gmail.com",
18 | "homepage": "http://ocramius.github.io/"
19 | },
20 | {
21 | "name": "Niklas Schöllhorn",
22 | "email": "schoellhorn.niklas@gmail.com",
23 | "role": "Maintainer"
24 | }
25 | ],
26 | "require": {
27 | "php": "~8.1.0 || ~8.2.0"
28 | },
29 | "require-dev": {
30 | "phpunit/phpunit": "^9.6.10",
31 | "doctrine/coding-standard": "^10.0.0",
32 | "vimeo/psalm": "^5.13.1",
33 | "roave/infection-static-analysis-plugin": "^1.32.0"
34 | },
35 | "autoload": {
36 | "psr-4": {
37 | "LazyProperty\\": "src/LazyProperty"
38 | }
39 | },
40 | "autoload-dev": {
41 | "psr-4": {
42 | "LazyPropertyTestAsset\\": "tests/LazyPropertyTestAsset",
43 | "LazyPropertyTest\\": "tests/LazyPropertyTest"
44 | }
45 | },
46 | "config": {
47 | "allow-plugins": {
48 | "dealerdirect/phpcodesniffer-composer-installer": true,
49 | "infection/extension-installer": false
50 | },
51 | "platform": {
52 | "php": "8.1.99"
53 | }
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/tests/LazyPropertyTestAsset/MixedPropertiesClass.php:
--------------------------------------------------------------------------------
1 | $propertyName;
28 | }
29 |
30 | /** @param string[] $properties */
31 | public function initProperties(array $properties): void
32 | {
33 | $this->initLazyProperties($properties);
34 | }
35 |
36 | private function getPrivate1(): string
37 | {
38 | return $this->private1 = 'private1';
39 | }
40 |
41 | private function getPrivate2(): string
42 | {
43 | return $this->private2 = 'private2';
44 | }
45 |
46 | protected function getProtected1(): string
47 | {
48 | return $this->protected1 = 'protected1';
49 | }
50 |
51 | protected function getProtected2(): string
52 | {
53 | return $this->protected2 = 'protected2';
54 | }
55 |
56 | public function getPublic1(): string
57 | {
58 | return $this->public1 = 'public1';
59 | }
60 |
61 | public function getPublic2(): string
62 | {
63 | return $this->public2 = 'public2';
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src/LazyProperty/Util/AccessScopeChecker.php:
--------------------------------------------------------------------------------
1 | isPublic()) {
34 | if (! isset($caller['object'])) {
35 | throw InvalidAccess::invalidContext(null, $instance, $property);
36 | }
37 |
38 | $caller = $caller['object'];
39 | $callerClass = $caller::class;
40 | $instanceClass = $instance::class;
41 |
42 | if (
43 | $callerClass === $instanceClass
44 | || ($reflectionProperty->isProtected() && is_subclass_of($callerClass, $instanceClass))
45 | || $callerClass === ReflectionProperty::class
46 | || is_subclass_of($callerClass, ReflectionProperty::class)
47 | ) {
48 | return;
49 | }
50 |
51 | throw InvalidAccess::invalidContext($caller, $instance, $property);
52 | }
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/LazyProperty/LazyPropertiesTrait.php:
--------------------------------------------------------------------------------
1 | indexed by property name */
23 | private array $lazyPropertyAccessors = [];
24 |
25 | /**
26 | * Initializes lazy properties so that first access causes their initialization via a getter
27 | *
28 | * @param string[] $lazyPropertyNames
29 | *
30 | * @throws MissingLazyPropertyGetter
31 | */
32 | private function initLazyProperties(array $lazyPropertyNames, bool $checkLazyGetters = true): void
33 | {
34 | foreach ($lazyPropertyNames as $lazyProperty) {
35 | if ($checkLazyGetters && ! method_exists($this, 'get' . $lazyProperty)) {
36 | throw MissingLazyPropertyGetter::fromGetter($this, 'get' . $lazyProperty, $lazyProperty);
37 | }
38 |
39 | $this->lazyPropertyAccessors[$lazyProperty] = false;
40 |
41 | if (isset($this->$lazyProperty)) {
42 | continue;
43 | }
44 |
45 | unset($this->$lazyProperty);
46 | }
47 | }
48 |
49 | /**
50 | * Magic getter - initializes and gets a property
51 | *
52 | * @throws ReflectionException
53 | */
54 | public function & __get(string $name): mixed
55 | {
56 | if (! isset($this->lazyPropertyAccessors[$name])) {
57 | throw InvalidLazyProperty::nonExistingLazyProperty($this, $name);
58 | }
59 |
60 | $caller = debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT)[1];
61 |
62 | // small optimization to avoid initializing reflection out of context
63 | if (! isset($caller['object']) || $caller['object'] !== $this) {
64 | AccessScopeChecker::checkCallerScope($caller, $this, $name);
65 | }
66 |
67 | $this->$name = null;
68 | $this->$name = $this->{'get' . $name}();
69 |
70 | return $this->$name;
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/.github/workflows/release-on-milestone-closed.yml:
--------------------------------------------------------------------------------
1 | # https://help.github.com/en/categories/automating-your-workflow-with-github-actions
2 |
3 | name: "Automatic Releases"
4 |
5 | on:
6 | milestone:
7 | types:
8 | - "closed"
9 |
10 | jobs:
11 | release:
12 | name: "GIT tag, release & create merge-up PR"
13 | runs-on: ubuntu-latest
14 |
15 | steps:
16 | - name: "Checkout"
17 | uses: "actions/checkout@v3"
18 |
19 | - name: "Release"
20 | uses: "laminas/automatic-releases@v1"
21 | with:
22 | command-name: "laminas:automatic-releases:release"
23 | env:
24 | "GITHUB_TOKEN": ${{ secrets.GITHUB_TOKEN }}
25 | "SIGNING_SECRET_KEY": ${{ secrets.SIGNING_SECRET_KEY }}
26 | "GIT_AUTHOR_NAME": ${{ secrets.GIT_AUTHOR_NAME }}
27 | "GIT_AUTHOR_EMAIL": ${{ secrets.GIT_AUTHOR_EMAIL }}
28 |
29 | - name: "Create Merge-Up Pull Request"
30 | uses: "laminas/automatic-releases@v1"
31 | with:
32 | command-name: "laminas:automatic-releases:create-merge-up-pull-request"
33 | env:
34 | "GITHUB_TOKEN": ${{ secrets.GITHUB_TOKEN }}
35 | "SIGNING_SECRET_KEY": ${{ secrets.SIGNING_SECRET_KEY }}
36 | "GIT_AUTHOR_NAME": ${{ secrets.GIT_AUTHOR_NAME }}
37 | "GIT_AUTHOR_EMAIL": ${{ secrets.GIT_AUTHOR_EMAIL }}
38 |
39 | - name: "Create and/or Switch to new Release Branch"
40 | uses: "laminas/automatic-releases@v1"
41 | with:
42 | command-name: "laminas:automatic-releases:switch-default-branch-to-next-minor"
43 | env:
44 | "GITHUB_TOKEN": ${{ secrets.ORGANIZATION_ADMIN_TOKEN }}
45 | "SIGNING_SECRET_KEY": ${{ secrets.SIGNING_SECRET_KEY }}
46 | "GIT_AUTHOR_NAME": ${{ secrets.GIT_AUTHOR_NAME }}
47 | "GIT_AUTHOR_EMAIL": ${{ secrets.GIT_AUTHOR_EMAIL }}
48 |
49 | - name: "Bump Changelog Version On Originating Release Branch"
50 | uses: "laminas/automatic-releases@v1"
51 | with:
52 | command-name: "laminas:automatic-releases:bump-changelog"
53 | env:
54 | "GITHUB_TOKEN": ${{ secrets.GITHUB_TOKEN }}
55 | "SIGNING_SECRET_KEY": ${{ secrets.SIGNING_SECRET_KEY }}
56 | "GIT_AUTHOR_NAME": ${{ secrets.GIT_AUTHOR_NAME }}
57 | "GIT_AUTHOR_EMAIL": ${{ secrets.GIT_AUTHOR_EMAIL }}
58 |
59 | - name: "Create new milestones"
60 | uses: "laminas/automatic-releases@v1"
61 | with:
62 | command-name: "laminas:automatic-releases:create-milestones"
63 | env:
64 | "GITHUB_TOKEN": ${{ secrets.GITHUB_TOKEN }}
65 | "SIGNING_SECRET_KEY": ${{ secrets.SIGNING_SECRET_KEY }}
66 | "GIT_AUTHOR_NAME": ${{ secrets.GIT_AUTHOR_NAME }}
67 | "GIT_AUTHOR_EMAIL": ${{ secrets.GIT_AUTHOR_EMAIL }}
68 |
--------------------------------------------------------------------------------
/tests/LazyPropertyTest/Util/AccessScopeCheckerTest.php:
--------------------------------------------------------------------------------
1 | $this], $this, 'backupGlobals');
24 |
25 | // Add to assertion count manually since we were successful when no exception was thrown and we got here.
26 | $this->addToAssertionCount(1);
27 | }
28 |
29 | public function testAllowsAccessToPublicProperties(): void
30 | {
31 | AccessScopeChecker::checkCallerScope(['object' => $this], new ParentClass(), 'public1');
32 |
33 | // Add to assertion count manually since we were successful when no exception was thrown and we got here.
34 | $this->addToAssertionCount(1);
35 | }
36 |
37 | public function testAllowsAccessFromSubClass(): void
38 | {
39 | AccessScopeChecker::checkCallerScope(
40 | ['object' => new InheritedPropertiesClass()],
41 | new ParentClass(),
42 | 'protected1',
43 | );
44 |
45 | // Add to assertion count manually since we were successful when no exception was thrown and we got here.
46 | $this->addToAssertionCount(1);
47 | }
48 |
49 | public function testAllowsAccessFromSameClass(): void
50 | {
51 | AccessScopeChecker::checkCallerScope(
52 | ['object' => new ParentClass()],
53 | new ParentClass(),
54 | 'private1',
55 | );
56 |
57 | // Add to assertion count manually since we were successful when no exception was thrown and we got here.
58 | $this->addToAssertionCount(1);
59 | }
60 |
61 | public function testAllowsAccessFromReflectionProperty(): void
62 | {
63 | AccessScopeChecker::checkCallerScope(
64 | ['object' => new ReflectionProperty(new ParentClass(), 'private1')],
65 | new ParentClass(),
66 | 'private1',
67 | );
68 |
69 | // Add to assertion count manually since we were successful when no exception was thrown and we got here.
70 | $this->addToAssertionCount(1);
71 | }
72 |
73 | public function testDisallowsAccessFromGlobalOrFunctionScope(): void
74 | {
75 | $this->expectException(InvalidAccess::class);
76 | AccessScopeChecker::checkCallerScope(
77 | [],
78 | new ParentClass(),
79 | 'private1',
80 | );
81 | }
82 |
83 | public function testDisallowsPrivateAccessFromDifferentScope(): void
84 | {
85 | $this->expectException(InvalidAccess::class);
86 | AccessScopeChecker::checkCallerScope(
87 | ['object' => $this],
88 | new ParentClass(),
89 | 'private1',
90 | );
91 | }
92 |
93 | public function testDisallowsProtectedAccessFromDifferentScope(): void
94 | {
95 | $this->expectException(InvalidAccess::class);
96 | AccessScopeChecker::checkCallerScope(
97 | ['object' => $this],
98 | new ParentClass(),
99 | 'private1',
100 | );
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Lazy Property
2 |
3 | This small library aims at providing a very simple and efficient loading of lazy properties
4 |
5 | [](https://packagist.org/packages/ocramius/lazy-property)
6 | [](https://packagist.org/packages/ocramius/lazy-property)
7 |
8 | ## Abandoned
9 |
10 | Starting with PHP 8.3, dynamic properties are [no longer allowed "out of the box"](https://wiki.php.net/rfc/deprecate_dynamic_properties).
11 | While it is still possible to have dynamic properties via explicit declaration (the `#[\AllowDynamicProperties]` attribute), the approach
12 | of this package is no longer to be considered safe nor efficient long-term.
13 |
14 | Based on that, this package is deprecated and abandoned: please use traditional PHP `array`s instead.
15 |
16 | ## Installation
17 |
18 | The suggested installation method is via [composer](https://getcomposer.org/):
19 |
20 | ```sh
21 | composer require ocramius/lazy-property
22 | ```
23 |
24 | ## Use case
25 |
26 | In many cases where lazy-initialization of private/protected properties is necessary,
27 | many people write classes that look like following:
28 |
29 | ```php
30 | class SomeService
31 | {
32 | protected $dependency;
33 |
34 | public function doWork()
35 | {
36 | $this->getDependency()->delegateWork();
37 | }
38 |
39 | protected function getDependency()
40 | {
41 | return $this->dependency ?: $this->dependency = get_dependency_somehow();
42 | }
43 | }
44 | ```
45 |
46 | This is problematic because implementors and people subclassing `SomeService` will eventually
47 | write:
48 |
49 | ```php
50 | class SomethingElse extends SomeService
51 | {
52 | public function doOtherWork()
53 | {
54 | $this->dependency->doMoreWork();
55 | }
56 | }
57 | ```
58 |
59 | This can work only if `SomeService#getDependency()` was called at least once upfront (which
60 | may well be under certain circumstances), and therefore is a cause of bugs/headaches/suicides/etc.
61 |
62 | In order to avoid this problem, the implementor of `SomeService` that is also exposing
63 | its protected `$dependency` property may just use `LazyProperty\LazyPropertiesTrait` to fix the problem:
64 |
65 |
66 | ```php
67 | #[\AllowDynamicProperties]
68 | class SomeService
69 | {
70 | use \LazyProperty\LazyPropertiesTrait;
71 |
72 | protected MyDependency $dependency;
73 |
74 | public function __construct()
75 | {
76 | $this->initLazyProperties(['dependency']);
77 | }
78 |
79 | public function doWork()
80 | {
81 | // look ma! no getter!
82 | $this->dependency->delegateWork();
83 | }
84 |
85 | protected function getDependency()
86 | {
87 | return $this->dependency ?: $this->dependency = get_dependency_somehow();
88 | }
89 | }
90 | ```
91 |
92 | With this, any access to `SomeService#$dependency` will cause a call to
93 | `SomeService#getDependency()` if the property was not already initialized.
94 |
95 |
96 | ```php
97 | class SomethingElse extends SomeService
98 | {
99 | public function doOtherWork()
100 | {
101 | // always works
102 | $this->dependency->doMoreWork();
103 | }
104 | }
105 | ```
106 |
107 | Please note that a getter is *required* in order for the property to be lazy.
108 |
109 | ## Performance notes
110 |
111 | Using `LazyProperty\LazyPropertiesTrait` allows to speed up applications where a massive
112 | amount of getter calls is going on in private/protected scope.
113 | There is some minor overhead in calling `SomeService#initLazyProperties()`, as well as in
114 | the first property access, but it should be negligible.
115 |
--------------------------------------------------------------------------------
/tests/LazyPropertyTest/LazyPropertiesTraitTest.php:
--------------------------------------------------------------------------------
1 | instance = new MixedPropertiesClass();
32 | }
33 |
34 | public function testMixedLazyPropertiesAreLazilyInitialized(): void
35 | {
36 | $instance = new MixedPropertiesClass();
37 |
38 | $this->assertNull($this->getProperty($instance, 'public1'));
39 | $this->assertNull($this->getProperty($instance, 'public2'));
40 | $this->assertNull($this->getProperty($instance, 'protected1'));
41 | $this->assertNull($this->getProperty($instance, 'protected2'));
42 | $this->assertNull($this->getProperty($instance, 'private1'));
43 | $this->assertNull($this->getProperty($instance, 'private2'));
44 |
45 | $instance->initProperties(['public1', 'protected1', 'private1']);
46 |
47 | $this->assertSame('public1', $this->getProperty($instance, 'public1'));
48 | $this->assertNull($this->getProperty($instance, 'public2'));
49 | $this->assertSame('protected1', $this->getProperty($instance, 'protected1'));
50 | $this->assertNull($this->getProperty($instance, 'protected2'));
51 | $this->assertSame('private1', $this->getProperty($instance, 'private1'));
52 | $this->assertNull($this->getProperty($instance, 'private2'));
53 | }
54 |
55 | public function testAllMixedLazyPropertiesAreLazilyInitialized(): void
56 | {
57 | $instance = new MixedPropertiesClass();
58 |
59 | $this->assertNull($this->getProperty($instance, 'public1'));
60 | $this->assertNull($this->getProperty($instance, 'public2'));
61 | $this->assertNull($this->getProperty($instance, 'protected1'));
62 | $this->assertNull($this->getProperty($instance, 'protected2'));
63 | $this->assertNull($this->getProperty($instance, 'private1'));
64 | $this->assertNull($this->getProperty($instance, 'private2'));
65 |
66 | $instance->initProperties(['public1', 'public2', 'protected1', 'protected2', 'private1', 'private2']);
67 |
68 | $this->assertSame('public1', $this->getProperty($instance, 'public1'));
69 | $this->assertSame('public2', $this->getProperty($instance, 'public2'));
70 | $this->assertSame('protected1', $this->getProperty($instance, 'protected1'));
71 | $this->assertSame('protected2', $this->getProperty($instance, 'protected2'));
72 | $this->assertSame('private1', $this->getProperty($instance, 'private1'));
73 | $this->assertSame('private2', $this->getProperty($instance, 'private2'));
74 | }
75 |
76 | public function testMixedLazyPropertiesAreLazilyInitializedWithProtectedAccess(): void
77 | {
78 | $instance = new MixedPropertiesClass();
79 |
80 | $this->assertNull($this->getProperty($instance, 'public1'));
81 | $this->assertNull($this->getProperty($instance, 'public2'));
82 | $this->assertNull($this->getProperty($instance, 'protected1'));
83 | $this->assertNull($this->getProperty($instance, 'protected2'));
84 | $this->assertNull($this->getProperty($instance, 'private1'));
85 | $this->assertNull($this->getProperty($instance, 'private2'));
86 |
87 | $instance->initProperties(['public1', 'protected1', 'private1']);
88 |
89 | $this->assertSame('public1', $instance->getProperty('public1'));
90 | $this->assertNull($instance->getProperty('public2'));
91 | $this->assertSame('protected1', $instance->getProperty('protected1'));
92 | $this->assertNull($instance->getProperty('protected2'));
93 | $this->assertSame('private1', $instance->getProperty('private1'));
94 | $this->assertNull($instance->getProperty('private2'));
95 | }
96 |
97 | public function testMixedInheritedLazyPropertiesAreLazilyInitialized(): void
98 | {
99 | $instance = new InheritedPropertiesClass();
100 |
101 | $this->assertNull($this->getProperty($instance, 'public1'));
102 | $this->assertNull($this->getProperty($instance, 'public2'));
103 | $this->assertNull($this->getProperty($instance, 'protected1'));
104 | $this->assertNull($this->getProperty($instance, 'protected2'));
105 |
106 | $instance->initProperties(['public1', 'protected1']);
107 |
108 | $this->assertSame('public1', $this->getProperty($instance, 'public1'));
109 | $this->assertNull($this->getProperty($instance, 'public2'));
110 | $this->assertSame('protected1', $this->getProperty($instance, 'protected1'));
111 | $this->assertNull($this->getProperty($instance, 'protected2'));
112 | }
113 |
114 | public function testThrowsExceptionOnMissingLazyGetter(): void
115 | {
116 | $instance = new MixedPropertiesClass();
117 |
118 | $this->expectException(MissingLazyPropertyGetter::class);
119 | $this->expectExceptionMessage('The getter "getnonExisting" for lazy property "nonExisting" is not defined');
120 | $instance->initProperties(['nonExisting']);
121 | }
122 |
123 | public function testDoesNotRaiseWarningsForNonExistingProperties(): void
124 | {
125 | $instance = new LazyGetterClass();
126 |
127 | $instance->initProperties(['property']);
128 |
129 | $this->assertSame('property', $instance->getProperty());
130 | }
131 |
132 | public function testDeniesAccessToNonExistingLazyProperties(): void
133 | {
134 | $this->expectException(InvalidLazyProperty::class);
135 | (new LazyGetterClass())->nonExisting;
136 | }
137 |
138 | public function testDeniesAccessToProtectedLazyProperties(): void
139 | {
140 | $instance = new MixedPropertiesClass();
141 |
142 | $instance->initProperties(['protected1']);
143 | $this->expectException(InvalidAccess::class);
144 | $instance->protected1;
145 | }
146 |
147 | public function testDeniesAccessToPrivateLazyProperties(): void
148 | {
149 | $instance = new MixedPropertiesClass();
150 |
151 | $instance->initProperties(['private1']);
152 | $this->expectException(InvalidAccess::class);
153 | $instance->private1;
154 | }
155 |
156 | public function testGetMultiInheritanceProperties(): void
157 | {
158 | $instanceA = new AClass();
159 | $instanceB = new BClass();
160 |
161 | $instanceA->initALazyProperties();
162 | $instanceB->initBLazyProperties();
163 |
164 | $this->assertSame(AClass::class, $this->getProperty($instanceA, 'private'));
165 | $this->assertSame(BClass::class, $this->getProperty($instanceB, 'private'));
166 | }
167 |
168 | public function testDoesNotReInitializeDefinedProperties(): void
169 | {
170 | $instance = new MixedPropertiesClass();
171 |
172 | $instance->public1 = 'defined';
173 |
174 | $instance->initProperties(['public1']);
175 |
176 | $this->assertSame('defined', $instance->public1);
177 | }
178 |
179 | /** @throws ReflectionException */
180 | private function getProperty(object $instance, string $propertyName): mixed
181 | {
182 | $reflectionClass = new ReflectionClass($instance);
183 |
184 | while ($reflectionClass && ! $reflectionClass->hasProperty($propertyName)) {
185 | $reflectionClass = $reflectionClass->getParentClass();
186 | }
187 |
188 | if (! $reflectionClass) {
189 | throw new UnexpectedValueException('Property "' . $propertyName . '" does not exist');
190 | }
191 |
192 | $reflectionProperty = $reflectionClass->getProperty($propertyName);
193 |
194 | $reflectionProperty->setAccessible(true);
195 |
196 | return $reflectionProperty->getValue($instance);
197 | }
198 | }
199 |
--------------------------------------------------------------------------------