I want to create a GenericCollection
class which is instantiated using another class as a template ("objects") and then performs task on it, such as loading objects from a database. My Collection basically looks like this:
final class GenericCollection {
private $template;
private $db;
public function __construct(CollectableInterface $template, DbInterface $db) {
$this->template = $template;
$this->db = $db;
}
public function load(array $filter): iterable {
$results = $this->db->read(['t' => $this->template::tableName()], $filter, $this->template::getFields());
foreach ($results as $row) {
yield $this->template::fromArray($row);
}
}
}
$collection = new GenericCollection(MyObject::template(), $db);
My issue is with actually providing the common functionality to my object classes. What I want to achieve is
fromArray
function should be somewhere once and be reusable in all the various object classes.I've tried the following designs (all of which uphold goal 1):
interface CollectableInterface {
public static function template(): object;
public static function getFields(): array;
public static function tableName(): string;
public static function fromArray(array $data): object;
}
trait CollectableTrait {
public static function fromArray(array $data = []): object {
foreach (get_object_vars($obj = new self) as $property => $default) {
$obj->$property = $data[$property] ?? $default;
}
return $obj;
}
public static function template(): object {
return new self;
}
}
final class MyObject implements CollectableInterface {
use CollectableTrait;
// properties, implementations of tableName() and getFields()
}
This doesn't work because the type hints are incompatible. There's actually no way to make them fit that I'm aware of. Using object
seems to be the only way to make the interface and the trait compatible (self
fails because the trait doesn't implement the interface, same issue if I explicitly type hint the interface), but then I can't actually type hint the interface in the Collection's constructor (same issue if I use the trait as the type hint, assuming that would even run).
The only way to make this approach work that I came up with is removing pretty much all the type hints, violating my second goal.
abstract class CollectableObject {
public static function fromArray(array $data = []): self {
foreach (get_object_vars($obj = new self) as $property => $default) {
$obj->$property = $data[$property] ?? $default;
}
return $obj;
}
public static function template(): self {
return new self;
}
abstract public static function getFields(): array;
abstract public static function tableName(): string;
}
final class MyObject extends CollectableObject {
// properties, implementations of tableName() and getFields()
}
and of course change the type hint in the Collection constructor to CollectableObject
.
This doesn't work because I can't instantiate the abstract class in the common methods (new self
).
This is essentially the same as the previous one, but the class isn't made abstract so the common methods can create an instance. This violates goal 3 as this class isn't supposed to be instantiable, but will be by calling CollectableObject::fromArray()
even if I make the constructor private to stop new CollectableObject
.
Is there any way to achieve all of my goals in this setup? Is my entire premise/overall structure flawed? Is there a different approach or some way to modify one of my approached so it works?
With either a trait or an abstract class, you could instantiate static
instead of self
:
trait builder {
public static function buildToo() {
return new static();
}
}
class Baz {
use builder;
}
$b = Baz::buildToo();
var_dump($b);
/* output:
object(Baz)#2 (0) {}
*/
abstract class Foo {
public static function build() {
return new static();
}
}
class Bar extends Foo {}
$a = Bar::build();
/* output:
object(Bar)#1 (0) {}
*/
These are forms of late static binding.
Starting with PHP 8, to be released later this month (Nov 2020), you will be even be able to declare static
as a return type hint:
abstract class Foo {
public static function build(): static
{
return new static();
}
}