Hello Internals,
As you may know, an inherited method cannot reduce the visibility of
an overridden method. For example, this results in a fatal error
during compilation:
class P {
public function hello($name = 'world') {
echo "hello $name\n";
}
}
class C extends P {
private function hello($name = 'world') {
parent::hello($name);
echo "goodbye $name\n";
}
}
However, we can make certain methods private anyway, namely,
constructors (I haven't gone hunting for other built-in methods yet).
This is perfectly allowed:
class P {
public function __construct($name = 'waldo') {
echo "hello $name\n";
}
}
class C extends P {
private function __construct($name = 'world') {
parent::__construct($name);
echo "goodbye $name\n";
}
}
To my somewhat trained eye, this appears to violate the Liskov
Substitution Principle, for example, this now can have hidden errors:
function create(P $class) {
return new (get_class($class))();
}
proven by:
$c = (new ReflectionClass(C::class))
->newInstanceWithoutConstructor();
create($c);
Even though we thought we knew that the constructor was declared public.
I'd like to propose an RFC to enforce the covariance of constructors
(just like is done for other methods), to take effect in PHP 9, with a
deprecation notice in 8.3.x.
I'm more than happy to implement it.
Does anyone feel strongly about this one way or the other?
Robert Landers
Software Engineer
Utrecht NL
I'd like to propose an RFC to enforce the covariance of constructors
(just like is done for other methods), to take effect in PHP 9, with a
deprecation notice in 8.3.x.
There's a lot more than visibility that is enforced on normal methods, but isn't on constructors. For instance this is also valid:
class A {
public function __construct(int $foo) {}
}
class B extends A {
public function __construct(string $bar) {}
}
From a theoretical perspective, I think the argument is roughly that classes aren't first-class citizens that you can pass around, so substitutability doesn't apply. You can't for instance write a function that explicitly depends on "a class definition inheriting from A", like this:
function foo(class<A> $class) {
$instance = new $class(42);
}
You can certainly simulate such code with some strings and maybe a bit of reflection, but the language isn't going to make any guarantees about it.
I did just think of a counter-example, though, which is that "new static($param)" is allowed, even though there's no way to know if $param will be accepted by subclasses. Maybe it shouldn't be allowed?
From a practical point of view, it's often very useful to sub-class something and provide a constructor with a different signature. Maybe your subclass has additional dependencies; maybe it can hard-code or calculate some of the inputs to the parent constructor for a special case, etc.
A private constructor can be used in conjunction with static methods to simulate multiple named constructors (createFromString, createFromRequest, etc). Given the lack of other guarantees, there's no particular gain in preventing that just because the parent class has a public constructor.
Regards,
--
Rowan Tommins
[IMSoP]
On Thu, Nov 23, 2023 at 5:31 PM Robert Landers landers.robert@gmail.com
wrote:
Hello Internals,
As you may know, an inherited method cannot reduce the visibility of
an overridden method. For example, this results in a fatal error
during compilation:class P {
public function hello($name = 'world') {
echo "hello $name\n";
}
}class C extends P {
private function hello($name = 'world') {
parent::hello($name);
echo "goodbye $name\n";
}
}However, we can make certain methods private anyway, namely,
constructors (I haven't gone hunting for other built-in methods yet).
This is perfectly allowed:class P {
public function __construct($name = 'waldo') {
echo "hello $name\n";
}
}class C extends P {
private function __construct($name = 'world') {
parent::__construct($name);
echo "goodbye $name\n";
}
}To my somewhat trained eye, this appears to violate the Liskov
Substitution Principle, for example, this now can have hidden errors:function create(P $class) {
return new (get_class($class))();
}proven by:
$c = (new ReflectionClass(C::class))
->newInstanceWithoutConstructor();create($c);
Even though we thought we knew that the constructor was declared public.
I'd like to propose an RFC to enforce the covariance of constructors
(just like is done for other methods), to take effect in PHP 9, with a
deprecation notice in 8.3.x.I'm more than happy to implement it.
Does anyone feel strongly about this one way or the other?
Constructors are an implementation detail of a specialized class and as
such they're not subject to LSP because the goal of LSP is to be able to
make sure that any object of a given type hierarchy can be used to
accomplish a certain behavior. If you take a step back from PHP's
dynamic nature and think about LSP from a more pure type system, the fact
you're expecting an object of type C, but then you completely disregard
everything about the object itself and dive into it's metadata to build
another object, that's the moment you're no longer playing by the rules of
OOP. It's like those mathematical equations that prove that 1 = 2, they all
have one thing in common: they end up dividing by 0 at some point.
OOP here dictates that you should reach for patterns like Builder, Abstract
Factory or similar. That way you constraint yourself to the rules of OOP
and you won't get weird outcomes.
From another point of view, when a type is expected by a function or
method, all we can expect from it is whatever was defined as the blueprint
(class/interface) of that object and the __construct() is a special method
that is not assumed to be part of that blueprint because it's not
reasonable to do $object->__construct();
after receiving an object. As
such, a constructor cannot break LSP because the constructor is not part of
the object's API from a "receptor" point of view.
I don't have a vote so take my opinion with a bucket of salt, but if I
could I would definitely vote against such RFC.
--
Marco Deleu
Hello Internals,
As you may know, an inherited method cannot reduce the visibility of
an overridden method. For example, this results in a fatal error
during compilation:class P {
public function hello($name = 'world') {
echo "hello $name\n";
}
}class C extends P {
private function hello($name = 'world') {
parent::hello($name);
echo "goodbye $name\n";
}
}However, we can make certain methods private anyway, namely,
constructors (I haven't gone hunting for other built-in methods yet).
This is perfectly allowed:class P {
public function __construct($name = 'waldo') {
echo "hello $name\n";
}
}class C extends P {
private function __construct($name = 'world') {
parent::__construct($name);
echo "goodbye $name\n";
}
}To my somewhat trained eye, this appears to violate the Liskov
Substitution Principle, for example, this now can have hidden errors:function create(P $class) {
return new (get_class($class))();
}proven by:
$c = (new ReflectionClass(C::class))
->newInstanceWithoutConstructor();create($c);
Even though we thought we knew that the constructor was declared public.
I'd like to propose an RFC to enforce the covariance of constructors
(just like is done for other methods), to take effect in PHP 9, with a
deprecation notice in 8.3.x.I'm more than happy to implement it.
Does anyone feel strongly about this one way or the other?
Constructors are an implementation detail of a specialized class and as such they're not subject to LSP because the goal of LSP is to be able to make sure that any object of a given type hierarchy can be used to accomplish a certain behavior. If you take a step back from PHP's dynamic nature and think about LSP from a more pure type system, the fact you're expecting an object of type C, but then you completely disregard everything about the object itself and dive into it's metadata to build another object, that's the moment you're no longer playing by the rules of OOP. It's like those mathematical equations that prove that 1 = 2, they all have one thing in common: they end up dividing by 0 at some point.
OOP here dictates that you should reach for patterns like Builder, Abstract Factory or similar. That way you constraint yourself to the rules of OOP and you won't get weird outcomes.
From another point of view, when a type is expected by a function or method, all we can expect from it is whatever was defined as the blueprint (class/interface) of that object and the __construct() is a special method that is not assumed to be part of that blueprint because it's not reasonable to do
$object->__construct();
after receiving an object. As such, a constructor cannot break LSP because the constructor is not part of the object's API from a "receptor" point of view.I don't have a vote so take my opinion with a bucket of salt, but if I could I would definitely vote against such RFC.
--
Marco Deleu
Thanks Marco,
That's an interesting perspective and one I would agree with for the
most part, especially if you take my illustration at face value. Where
it gets weird/breaks down is when you have a class-string, that you
assert is the correct type, and then try to instantiate it:
// from somewhere
$class = "C";
if(is_subclass_of($class, P::class)) {
$example = new $class("world");
}
If PHP didn't offer these built-in methods, then I would fully agree
with you, but it does, which puts it into a weird position where
sometimes a class is substitutable, and in this one special case, it
is not.
Robert Landers
Software Engineer
Utrecht NL
Constructors are an implementation detail of a specialized class and as such they're not subject to LSP because the goal of LSP is to be able to make sure that any object of a given type hierarchy can be used to accomplish a certain behavior. If you take a step back from PHP's dynamic nature and think about LSP from a more pure type system, the fact you're expecting an object of type C, but then you completely disregard everything about the object itself and dive into it's metadata to build another object, that's the moment you're no longer playing by the rules of OOP. It's like those mathematical equations that prove that 1 = 2, they all have one thing in common: they end up dividing by 0 at some point.
OOP here dictates that you should reach for patterns like Builder, Abstract Factory or similar. That way you constraint yourself to the rules of OOP and you won't get weird outcomes.
From another point of view, when a type is expected by a function or method, all we can expect from it is whatever was defined as the blueprint (class/interface) of that object and the __construct() is a special method that is not assumed to be part of that blueprint because it's not reasonable to do
$object->__construct();
after receiving an object. As such, a constructor cannot break LSP because the constructor is not part of the object's API from a "receptor" point of view.I don't have a vote so take my opinion with a bucket of salt, but if I could I would definitely vote against such RFC.
--
Marco DeleuThanks Marco,
That's an interesting perspective and one I would agree with for the
most part, especially if you take my illustration at face value. Where
it gets weird/breaks down is when you have a class-string, that you
assert is the correct type, and then try to instantiate it:// from somewhere
$class = "C";if(is_subclass_of($class, P::class)) {
$example = new $class("world");
}If PHP didn't offer these built-in methods, then I would fully agree
with you, but it does, which puts it into a weird position where
sometimes a class is substitutable, and in this one special case, it
is not.
What Marco said pretty much mirrors the answer in Software Engineering SE:
https://softwareengineering.stackexchange.com/a/270738/9114 https://softwareengineering.stackexchange.com/a/270738/9114
As if probably obvious now, when you are using a variable containing a class name to instantiate a class you are actually not using OOP, you are using the text expansion capabilities of PHP. The fact that it appears to be similar to a method call is just likely coincidence,
Now if classes were first class objects in PHP, then there might be an argument, but alas, they are not.
Anyway, I always addressed the problem you are running into by defining a make_new()
static method. Here's what your example might look after an initial refactor:
class P {
public $msg;
function __construct($msg){
$this->msg = $msg;
}
}
class C extends P {
static function make_new($props){
return new C($props["msg"]);
}
}
$class = "C";
if(is_subclass_of($class, P::class)) {
$example = $class::make_new( ["msg"=>"world"] );
print_r($example);
}
Of course, getting it to production code will take a lot more, like you can see here (although this code has now been update for probably five years since I am no longer coding in PHP professionally):
https://github.com/search?q=repo%3Awplib%2Fwplib%20make_new&type=code https://github.com/search?q=repo:wplib/wplib%20make_new&type=code
And if you are dealing with 3rd party classes, you'll need to write wrapper classes.
Hope this helps.
-Mike
Constructors are an implementation detail of a specialized class and as such they're not subject to LSP because the goal of LSP is to be able to make sure that any object of a given type hierarchy can be used to accomplish a certain behavior. If you take a step back from PHP's dynamic nature and think about LSP from a more pure type system, the fact you're expecting an object of type C, but then you completely disregard everything about the object itself and dive into it's metadata to build another object, that's the moment you're no longer playing by the rules of OOP. It's like those mathematical equations that prove that 1 = 2, they all have one thing in common: they end up dividing by 0 at some point.
OOP here dictates that you should reach for patterns like Builder, Abstract Factory or similar. That way you constraint yourself to the rules of OOP and you won't get weird outcomes.
From another point of view, when a type is expected by a function or method, all we can expect from it is whatever was defined as the blueprint (class/interface) of that object and the __construct() is a special method that is not assumed to be part of that blueprint because it's not reasonable to do
$object->__construct();
after receiving an object. As such, a constructor cannot break LSP because the constructor is not part of the object's API from a "receptor" point of view.I don't have a vote so take my opinion with a bucket of salt, but if I could I would definitely vote against such RFC.
--
Marco DeleuThanks Marco,
That's an interesting perspective and one I would agree with for the
most part, especially if you take my illustration at face value. Where
it gets weird/breaks down is when you have a class-string, that you
assert is the correct type, and then try to instantiate it:// from somewhere
$class = "C";if(is_subclass_of($class, P::class)) {
$example = new $class("world");
}If PHP didn't offer these built-in methods, then I would fully agree
with you, but it does, which puts it into a weird position where
sometimes a class is substitutable, and in this one special case, it
is not.What Marco said pretty much mirrors the answer in Software Engineering SE:
https://softwareengineering.stackexchange.com/a/270738/9114
As if probably obvious now, when you are using a variable containing a class name to instantiate a class you are actually not using OOP, you are using the text expansion capabilities of PHP. The fact that it appears to be similar to a method call is just likely coincidence,
Now if classes were first class objects in PHP, then there might be an argument, but alas, they are not.
Anyway, I always addressed the problem you are running into by defining a
make_new()
static method. Here's what your example might look after an initial refactor:class P {
public $msg;
function __construct($msg){
$this->msg = $msg;
}
}
class C extends P {
static function make_new($props){
return new C($props["msg"]);
}
}$class = "C";
if(is_subclass_of($class, P::class)) {
$example = $class::make_new( ["msg"=>"world"] );
print_r($example);
}Of course, getting it to production code will take a lot more, like you can see here (although this code has now been update for probably five years since I am no longer coding in PHP professionally):
https://github.com/search?q=repo%3Awplib%2Fwplib%20make_new&type=code
And if you are dealing with 3rd party classes, you'll need to write wrapper classes.
Hope this helps.
-Mike
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 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
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 8And 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 usepush()
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
Hello internals,
I won't be pursuing this as an RFC. It seems either an abstract class
or interface can force users to obey LSP, but it is purely opt-in. It
strikes a good balance, but does create some surprises when someone
comes along with a bug they wrote themselves into without realizing
it.
Thank you all for your discussion!
Let's ignore for a second 28 years of breaking change and focus on the OOP principle
I wish more people had that attitude on this list. :D I'm of the
opinion that you first decide to do something, then figure out how to
make it backward-compatible and maintain it. Take annotations, for
example, it was wanted, and then it was figured out how to (cleverly)
make them backward compatible. If people had gotten hung up on
backward compatibility or maintenance from the beginning, I doubt it
would have ever made it past that point. Now, annotations are
increasingly useful in PHP, from describing how to serialize an
object, to validations, to guards.
Robert Landers
Software Engineer
Utrecht NL
On Thu, Nov 23, 2023 at 12:31 PM Robert Landers
landers.robert@gmail.com wrote:
Hello Internals,
As you may know, an inherited method cannot reduce the visibility of
an overridden method. For example, this results in a fatal error
during compilation:class P {
public function hello($name = 'world') {
echo "hello $name\n";
}
}class C extends P {
private function hello($name = 'world') {
parent::hello($name);
echo "goodbye $name\n";
}
}However, we can make certain methods private anyway, namely,
constructors (I haven't gone hunting for other built-in methods yet).
This is perfectly allowed:class P {
public function __construct($name = 'waldo') {
echo "hello $name\n";
}
}class C extends P {
private function __construct($name = 'world') {
parent::__construct($name);
echo "goodbye $name\n";
}
}To my somewhat trained eye, this appears to violate the Liskov
Substitution Principle, for example, this now can have hidden errors:function create(P $class) {
return new (get_class($class))();
}proven by:
$c = (new ReflectionClass(C::class))
->newInstanceWithoutConstructor();create($c);
Even though we thought we knew that the constructor was declared public.
I'd like to propose an RFC to enforce the covariance of constructors
(just like is done for other methods), to take effect in PHP 9, with a
deprecation notice in 8.3.x.I'm more than happy to implement it.
Does anyone feel strongly about this one way or the other?
Robert Landers
Software Engineer
Utrecht NL
Hi Robert,
Liskov and Wing explicitly exempted constructors in their original
paper describing the principle:
http://reports-archive.adm.cs.cmu.edu/anon/1999/CMU-CS-99-156.pdf
4.1 Type Specifications
A type specification includes the following information:
- The type's name
- A description of the type's value space
- A definition of the type's invariant and history properties
- For each of the type's methods:
- Its name;
- Its signature including signaled exceptions;
- Its behavior in terms of pre-conditions and post-conditions.
Note that the creators are missing. Omitting creators allows subtypes to provide different creators than their supertypes. In addition, omitting creators makes it easy for a type to have multiple implementations, allows new creators to be added later, and re ects common usage: for example, Java interfaces and virtual types provide no way for users to create objects of the type.
(Creators is Liskov's word for constructors, which they later define
in the paper.)
Hope that helps.
- Mark Trapp
Hello Internals,
As you may know, an inherited method cannot reduce the visibility of
an overridden method. For example, this results in a fatal error
during compilation:class P {
public function hello($name =3D 'world') {
echo "hello $name\n";
}
}class C extends P {
private function hello($name =3D 'world') {
parent::hello($name);
echo "goodbye $name\n";
}
}However, we can make certain methods private anyway, namely,
constructors (I haven't gone hunting for other built-in methods yet).
This is perfectly allowed:class P {
public function __construct($name =3D 'waldo') {
echo "hello $name\n";
}
}class C extends P {
private function __construct($name =3D 'world') {
parent::__construct($name);
echo "goodbye $name\n";
}
}To my somewhat trained eye, this appears to violate the Liskov
Substitution Principle, for example, this now can have hidden errors:function create(P $class) {
return new (get_class($class))();
}proven by:
$c =3D (new ReflectionClass(C::class))
->newInstanceWithoutConstructor();create($c);
Even though we thought we knew that the constructor was declared public.
I'd like to propose an RFC to enforce the covariance of constructors
(just like is done for other methods), to take effect in PHP 9, with a
deprecation notice in 8.3.x.I'm more than happy to implement it.
Does anyone feel strongly about this one way or the other?
Robert Landers
Software Engineer
Utrecht NL
(Apologies: resending because I used the wrong email address to send
to the list)
Hi Robert,
Liskov and Wing explicitly exempted constructors in their original
paper describing the principle:
http://reports-archive.adm.cs.cmu.edu/anon/1999/CMU-CS-99-156.pdf
4.1 Type Specifications
A type specification includes the following information:
- The type's name
- A description of the type's value space
- A definition of the type's invariant and history properties
- For each of the type's methods:
- Its name;
- Its signature including signaled exceptions;
- Its behavior in terms of pre-conditions and post-conditions.
Note that the creators are missing. Omitting creators allows subtypes to =
provide different creators than their supertypes. In addition, omitting cre=
ators makes it easy for a type to have multiple implementations, allows new=
creators to be added later, and re ects common usage: for example, Java in=
terfaces and virtual types provide no way for users to create objects of th=
e type.
(Creators is Liskov's word for constructors, which they later define
in the paper.)
Hope that helps.
- Mark Trapp
Hello Internals,
As you may know, an inherited method cannot reduce the visibility of
an overridden method. For example, this results in a fatal error
during compilation:class P {
public function hello($name =3D 'world') {
echo "hello $name\n";
}
}class C extends P {
private function hello($name =3D 'world') {
parent::hello($name);
echo "goodbye $name\n";
}
}However, we can make certain methods private anyway, namely,
constructors (I haven't gone hunting for other built-in methods yet).
This is perfectly allowed:class P {
public function __construct($name =3D 'waldo') {
echo "hello $name\n";
}
}class C extends P {
private function __construct($name =3D 'world') {
parent::__construct($name);
echo "goodbye $name\n";
}
}To my somewhat trained eye, this appears to violate the Liskov
Substitution Principle, for example, this now can have hidden errors:function create(P $class) {
return new (get_class($class))();
}proven by:
$c =3D (new ReflectionClass(C::class))
->newInstanceWithoutConstructor();create($c);
Even though we thought we knew that the constructor was declared public.
I'd like to propose an RFC to enforce the covariance of constructors
(just like is done for other methods), to take effect in PHP 9, with a
deprecation notice in 8.3.x.I'm more than happy to implement it.
Does anyone feel strongly about this one way or the other?
Robert Landers
Software Engineer
Utrecht NL(Apologies: resending because I used the wrong email address to send
to the list)Hi Robert,
Liskov and Wing explicitly exempted constructors in their original
paper describing the principle:
http://reports-archive.adm.cs.cmu.edu/anon/1999/CMU-CS-99-156.pdf4.1 Type Specifications
A type specification includes the following information:
- The type's name
- A description of the type's value space
- A definition of the type's invariant and history properties
- For each of the type's methods:
- Its name;
- Its signature including signaled exceptions;
- Its behavior in terms of pre-conditions and post-conditions.
Note that the creators are missing. Omitting creators allows subtypes to =
provide different creators than their supertypes. In addition, omitting cre=
ators makes it easy for a type to have multiple implementations, allows new=
creators to be added later, and re ects common usage: for example, Java in=
terfaces and virtual types provide no way for users to create objects of th=
e type.(Creators is Liskov's word for constructors, which they later define
in the paper.)Hope that helps.
- Mark Trapp
Thanks Mark,
That does help quite a bit! Thanks!
I might think they are wrong, but if so, I'll probably need to write a
paper and prove it before I come back. :D
Cheers!
Rob
Hi Robert,
Am 23.11.23 um 21:31 schrieb Robert Landers:
However, we can make certain methods private anyway, namely,
constructors (I haven't gone hunting for other built-in methods yet).
This is perfectly allowed:class P {
public function __construct($name = 'waldo') {
echo "hello $name\n";
}
}class C extends P {
private function __construct($name = 'world') {
parent::__construct($name);
echo "goodbye $name\n";
}
}
you can enforce a public constructor by using an interface:
interface X {
public function __construct (int $foo);
}
class A implements X {
public function __construct (int $foo) {
echo __CLASS__ . ": $foo\n";
}
}
class B extends A {
private function __construct (int $foo) {
echo __CLASS__ . ": $foo\n";
}
}
Which will give you the following error:
PHP Fatal error: Access level to B::__construct() must be public (as in
class X) in test.php on line 14
That way you put the definition of the constructor into the contract.
Greets
Dennis