<?php
# Part 1: Prepare the classes. There should me lots of more code to make it robust, but I'm trying the PoC simple.
class Repository
{
public array $data;
/**
* Works out the namespaced key and stores the value twice.
*
* If we're storing objects, they're stored as a reference anyway, so it doesn't matter that the same thing is there twice.
*
* If we store a scalar, then yes, we take up 2x the memory, but I think it's not a common use case and it's not a big deal.
*/
public function setReference(string $key, mixed $identity): void
{
// Stores the last $identity set with $key. If you don't re-use keys, this always keeps your object. Previous behaviour.
$this->data[$key] = $identity;
// Stores the namespaced identity. If you re-use keys and use the type as discriminator, this is what you'll be getting things back from.
if (is_object($identity)) {
$nsKey = $key.$this->getRealClass($identity::class);
} else {
$nsKey = $key.(get_debug_type($identity) ?? '');
}
$this->data[$nsKey] = $identity;
}
/**
* The @template notation below tells static analysers what value is returned when $type is provided. I tested this with PHPStorm - $boss = $repository->getReference('foo', Boss::class) correctly identifies $boss as Boss::class object.
*
* @template T
* @param string $key
* @param class-string<T> $type Could be a class name, or even a 'string' or 'int' value.
* @return T
*/
public function getReference(string $key, string $type = null)
{
// If the $key doesn't exist, it doesn't matter what $type we might be asking for: the $key was never used when setting a value
if (!array_key_exists($key, $this->data)) {
throw new \Exception('Fixture not found');
}
// No $type provided or not found using the namespaced key
if (!$type) {
return $this->data[$key];
}
// The correct $key and $type combination exist - all good!
if (array_key_exuists($key.$type, $this->data)) {
// I'd assert the type here too, but for sake of example simplicity of this PoC, let's just not
return $this->data[$key.$type];
}
// If we made it here, the $key was used to set a value, but the $key we are asking for is not what the object was determined to be when it was being set.
// The not-namespaced key happens to be of the correct type - this covers storing the Employee::class and asking for Person::class
if (class_exists($type) && $this->data[$key] instanceof $type) {
return $this->data[$key];
}
// This means the last object stored using $key was not of $type. Since we're clearly saying we want a $type, we can't return anything
throw new \Exception(sprintf(
'Fixture %s was found, but is type %s, not %s. Please ask for the correct type.',
$key,
get_debug_type($this->data[$key]),
$type,
));
}
protected function getRealClass($className)
{
if (substr($className, -5) === 'Proxy') {
return substr($className, 0, -5);
}
return $className;
}
}
abstract class Person {
public function __construct(public string $name) {}
}
class Boss extends Person {}
class Employee extends Person {}
# Part 2: Basic use case PoC
$repository = new Repository();
$bossJohn = new Boss('John');
$bossJane = new Boss('Jane');
$employeeJim = new Employee('Jim');
$repository->setReference('boss', $bossJohn);
$repository->setReference('boss', $bossJane); // overwrites 'boss' key with Jane because John and Jane are the same class
assert($repository->getReference('boss', Boss::class) === $bossJane); // Retrieves last set boss that overwrites previous bosses
assert($repository->getReference('boss') === $bossJane); // retrieves the non-namespaced value
$repository->setReference('person', $bossJohn);
$repository->setReference('person', $employeeJim); // adds Jim under duplicate 'person' key because of different classes
assert($repository->getReference('person', Boss::class) === $bossJohn); // retrieves the correct person
assert($repository->getReference('person', Employee::class) === $employeeJim); // retrieves the correct person
assert($repository->getReference('person') === $employeeJim);
# Part 3: PoC of things that I don't think are possible if https://github.com/doctrine/data-fixtures/pull/409 gets merged as is on 8th Feb 2023:
// This is where things get intereesting, you can store other things than objects, if you want. Think of testing encryption keys generated on the fly etc.
$repository->setReference('number', 31337);
assert($repository->getReference('number') === 31337);
$repository->setReference('number', 31337.0);
assert($repository->getReference('number', 'float') === 31337.0);
// Using this method we can use any parent in the inheritance structure
assert($repository->getReference('person', Person::class) === $employeeJim); // Returns the last set 'person' even thought we're using a parent Person::class, not Employee::class
// Using this method we can use any parent in the inheritance structure
assert($repository->getReference('person', Boss::class) === $bossJohn); // Returns the correct fixture that duplicates keys, as long as we know what is the exact class we're after
echo 'Success.';
Here you find the average performance (time & memory) of each version. A grayed out version indicates it didn't complete successfully (based on exit-code).