>
> 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