diff --git a/Tests/PackageTest.php b/Tests/PackageTest.php index c86e3c42..c0949ad9 100755 --- a/Tests/PackageTest.php +++ b/Tests/PackageTest.php @@ -7,6 +7,8 @@ namespace Joomla\Github\Tests; use Joomla\Github\Github; +use Joomla\Github\Package\Issues; +use Joomla\Github\Package\Repositories\Forks; use Joomla\Github\Tests\Stub\GitHubTestCase; /** @@ -65,4 +67,74 @@ public function testGetInvalid() $this->object->repositories->INVALID; } + + /** + * Tests that the lazily created objects are cached without dynamic properties + * + * @return void + */ + public function testGetCachesWithoutDynamicProperties() + { + $errors = []; + + set_error_handler( + static function (int $errno, string $errstr) use (&$errors): bool { + $errors[] = $errstr; + + return true; + } + ); + + try { + $repositories = $this->object->repositories; + $forks = $this->object->repositories->forks; + + $this->assertSame($repositories, $this->object->repositories); + $this->assertSame($forks, $this->object->repositories->forks); + } finally { + restore_error_handler(); + } + + $this->assertSame([], $errors); + } + + /** + * Tests that API objects can be replaced, e.g. with mocks + * + * @return void + */ + public function testSetReplacesObjects() + { + $issues = $this->createStub(Issues::class); + $forks = $this->createStub(Forks::class); + + $this->object->issues = $issues; + $this->object->repositories->forks = $forks; + + $this->assertSame($issues, $this->object->issues); + $this->assertSame($forks, $this->object->repositories->forks); + } + + /** + * Tests the magic __isset and __unset methods + * + * @return void + */ + public function testIssetAndUnset() + { + $this->assertFalse(isset($this->object->issues)); + $this->assertFalse(isset($this->object->repositories->forks)); + + $issues = $this->object->issues; + $forks = $this->object->repositories->forks; + + $this->assertTrue(isset($this->object->issues)); + $this->assertTrue(isset($this->object->repositories->forks)); + + unset($this->object->issues, $this->object->repositories->forks); + + $this->assertFalse(isset($this->object->issues)); + $this->assertNotSame($issues, $this->object->issues); + $this->assertNotSame($forks, $this->object->repositories->forks); + } } diff --git a/docs/overview.md b/docs/overview.md index cfd53634..b777f0fc 100644 --- a/docs/overview.md +++ b/docs/overview.md @@ -1,3 +1,17 @@ # Github API package TODO + +## Package objects + +`Github` creates the package objects (`$github->issues`, `$github->repositories`, …) and their sub-packages +(`$github->repositories->forks`) on first access and reuses them afterwards. They can be replaced, for example with a +mock in a test: + +```php +$github->issues = $issuesMock; +$github->repositories->forks = $forksMock; +``` + +`isset($github->issues)` tells whether the object has been created or set; `unset($github->issues)` makes the next +access create a new one. diff --git a/src/AbstractPackage.php b/src/AbstractPackage.php index 71266b7c..5da93f01 100644 --- a/src/AbstractPackage.php +++ b/src/AbstractPackage.php @@ -19,6 +19,16 @@ */ abstract class AbstractPackage extends AbstractGithubObject { + /** + * The lazily created API objects, keyed by name. + * + * Kept in an array instead of dynamic properties, which are deprecated since PHP 8.2. + * + * @var array + * @since __DEPLOY_VERSION__ + */ + private array $packages = []; + /** * Constructor. * @@ -47,6 +57,10 @@ public function __construct(Registry $options = null, Http $client = null) */ public function __get($name) { + if (isset($this->packages[$name])) { + return $this->packages[$name]; + } + $class = '\\Joomla\\Github\\Package\\' . $this->package . '\\' . ucfirst($name); if (class_exists($class) == false) { @@ -60,10 +74,49 @@ public function __get($name) ); } - if (isset($this->$name) == false) { - $this->$name = new $class($this->options, $this->client); - } + return $this->packages[$name] = new $class($this->options, $this->client); + } - return $this->$name; + /** + * Magic method to replace an API object, e.g. with a mock in tests + * + * @param string $name Name of the property + * @param mixed $value The object to use + * + * @return void + * + * @since __DEPLOY_VERSION__ + */ + public function __set($name, $value) + { + $this->packages[$name] = $value; + } + + /** + * Magic method to check whether an API object has been created or set + * + * @param string $name Name of the property + * + * @return boolean + * + * @since __DEPLOY_VERSION__ + */ + public function __isset($name) + { + return isset($this->packages[$name]); + } + + /** + * Magic method to remove an API object, so that it is created again on the next access + * + * @param string $name Name of the property + * + * @return void + * + * @since __DEPLOY_VERSION__ + */ + public function __unset($name) + { + unset($this->packages[$name]); } } diff --git a/src/Github.php b/src/Github.php index 632c54e4..cb626d43 100644 --- a/src/Github.php +++ b/src/Github.php @@ -53,6 +53,16 @@ class Github */ protected $client; + /** + * The lazily created API objects, keyed by name. + * + * Kept in an array instead of dynamic properties, which are deprecated since PHP 8.2. + * + * @var array + * @since __DEPLOY_VERSION__ + */ + private array $packages = []; + /** * Constructor. * @@ -90,19 +100,62 @@ public function __construct(Registry $options = null, Http $client = null) */ public function __get($name) { + if (isset($this->packages[$name])) { + return $this->packages[$name]; + } + $class = 'Joomla\\Github\\Package\\' . ucfirst($name); if (class_exists($class)) { - if (isset($this->$name) == false) { - $this->$name = new $class($this->options, $this->client); - } - - return $this->$name; + return $this->packages[$name] = new $class($this->options, $this->client); } throw new \InvalidArgumentException(sprintf('Argument %s produced an invalid class name: %s', $name, $class)); } + /** + * Magic method to replace an API object, e.g. with a mock in tests + * + * @param string $name Name of the property + * @param mixed $value The object to use + * + * @return void + * + * @since __DEPLOY_VERSION__ + */ + public function __set($name, $value) + { + $this->packages[$name] = $value; + } + + /** + * Magic method to check whether an API object has been created or set + * + * @param string $name Name of the property + * + * @return boolean + * + * @since __DEPLOY_VERSION__ + */ + public function __isset($name) + { + return isset($this->packages[$name]); + } + + /** + * Magic method to remove an API object, so that it is created again on the next access + * + * @param string $name Name of the property + * + * @return void + * + * @since __DEPLOY_VERSION__ + */ + public function __unset($name) + { + unset($this->packages[$name]); + } + /** * Get an option from the GitHub instance. *