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

Reply via email to