> > Hey Mike, > > Using a static method to enforce LSP when it should have been enforced > in the first place, merely proves my point that it violates LSP. I > don't know how else to spell it out, I guess we can try this one next: > > class A { > final public static function foo() { > return new static(); > } > } > > class B extends A { > public function __construct(private string $name) {} > } > > $a = B::foo(); > > It just plainly violates LSP and you can't write safe code that > touches constructors. I don't know how else to spell this out. >
Hey Robert, as we've established we may not agree on this subject and I really don't want to sound like I want to convince you, so feel free to ignore my email or reply that this isn't helping. My goal here is merely to try and address what I think the error is in case it gives you some clarity. ----- In this snippet, you're using `new static()` which again is something PHP-specific and not 100% OOP definition. PHP has a friendly syntax to help you figure out what the class name is during runtime, this again is metadata. With these tools, I believe you are able to accomplish what you want by explicitly adding the constructor to your public API. ``` abstract class Template { abstract public function __construct(string $explicitlyDefinitionOfPublicAPI); } class A extends Template { public function __construct(string|null $ignore) {} final public static function foo() { return new static('parameter defined from A'); } } class B extends A { public function __construct(public string $name) {} } $b = B::foo('test'); var_dump($b->name); ``` Let's break this down. The `Template` class uses the Abstract Template pattern to define a public interface of the inheritance chain. As such, it offers the ability to opt-in to something that isn't standard: The Constructor method being part of a class Public API. Two things happen from this. We are forced to implement a constructor on class A otherwise we get: Fatal error: Class A contains 1 abstract method and must therefore be declared abstract or implement the remaining methods (Template::__construct) in /in/7qX9j on line 8 And we inherit the Constructor from A in B or we need to override it. My conclusion here is that 1) if you know what you're doing and 2) you want to make a constructor's object part of your public API, and therefore, respect LSP, you are free to do so. - Why shouldn't this be the default behavior in PHP? Let's ignore for a second 28 years of breaking change and focus on the OOP principle ``` <?php abstract class Queue { final public function serialize(array $message): string { return json_encode($message); } abstract public function push(array $message): void; } final class SqsQueue extends Queue { public function __constructor(private \Aws\Sqs\Client $client, private string $queue) {} public function push(array $message): void { $data = $this->serialize($message); $client->sendMessage([ 'QueueUrl' => $this->queue, 'Message' => $data, ]); } } final class RedisQueue extends Queue { public function __constructor(private \Illuminate\Redis\Connectors\PhpRedisConnector $connector, private array $configuration, private array $options) {} public function push(array $message): void { $connection = $this->connector->connect($this->configuration, $this->options); $data = $this->serialize($message); $command = "redis.call('rpush', KEYS[1], ARGV[1])"; $connection->eval($command, 1, 'queues:my-queue', $data); } } ``` This is a text-book case of LSP. A class that expects to receive a `Queue` object can freely use `push()` in a consistent and predictable manner, allowing substitution as it sees fit. The object constructor is exempt from LSP because it is the implementation detail of a particular class. RedisQueue needs to be able to communicate with Redis in order to provide a queueing capability. SqsQueue needs to be able to communicate with AWS SQS in order to provide a queuing capability. They have different dependencies/configurations and they wouldn't be able to perform their capabilities if the language forced their constructor to follow a single compatible signature. ----- In conclusion, I think PHP has the best of both worlds. We get little helpers to accommodate how OOP looks like in a dynamic script language (i.e. new static()) and we have a fully functioning LSP that allows you to take advantage of it however you see fit. The Queue example goes to show why having a constructor as part of the public API of a class hierarchy would be extremely bad, but PHP is nice enough to let you opt-in to it if you have reasons to force a class hierarchy to have a single dependency signature. -- Marco Deleu