Hi again, internals
My marathon of some crazy ideas continues :D, with less crazy one this time.
Idea
Allow "reimplementing" the non-static public API (that is public
properties and methods, excluding constructor) of a class by other
classes like this:
final class interface A {
public string $s;
public function __construct(string $s) { $this->s = $s; }
public static function fromInt(int $i): self { return new
self((string) $i); }
public function foo(): int { return 42; }
}
final class B implements A {
public string $s = 'hello';
public function foo(): int { return 69; }
}
function usesA(A $param): void {}
usesA(new B); // this works
Explanation
Consider there is a class like this:
final class Service {
public function __construct(private SomeDependency $dependency) {}
// ...
}
final class SomeDependency {
// ...
}
We want to write some tests for the Service class, but we don't want
to use a real SomeDependency instance
during tests. A common approach is to either extract an interface
(JUST to make it testable), or to drop the
final
keyword and allow extending the class.
Both approaches have their flaws:
- extracting an interface unnecessarily complicates the system, where
only one "real" implementation of an interface is assumed. - dropping the
final
keyword allows for the rabbit-hole of
inheritance abuse, like greatly described in this article:
https://front-line-php.com/object-oriented
I believe I came up with a better idea: what if we could leave both
benefits of prohibiting the inheritance abuse and also allow not to
clutter our namespace with excess entities like interfaces? I hereby
suggest to combine the responsibilities of a class and an interface
into one thing like that:
final class interface C {}
final class D implements C {}
Now other classes can "implement" this class as well. Introduction of
the new syntax (class interface
) also solves BC problem - if you
want to forbid your classes to be reimplemented whatsoever, you can
still stick to the final class
syntax. Although it is also possible
to allow "reimplementing" ANY class, then new syntax is not needed -
but some library writers like Marco Pivetta could be sad about that I
suppose.
Soo..., what do you think? Could this be a valuable addition to the language?
Regards,
Illia / someniatko
Hi again, internals
My marathon of some crazy ideas continues :D, with less crazy one this
time.Idea
Allow "reimplementing" the non-static public API (that is public
properties and methods, excluding constructor) of a class by other
classes like this:final class interface A { public string $s; public function __construct(string $s) { $this->s = $s; } public static function fromInt(int $i): self { return new self((string) $i); } public function foo(): int { return 42; } } final class B implements A { public string $s = 'hello'; public function foo(): int { return 69; } } function usesA(A $param): void {} usesA(new B); // this works
Explanation
Consider there is a class like this:
final class Service { public function __construct(private SomeDependency $dependency) {} // ... } final class SomeDependency { // ... }
We want to write some tests for the Service class, but we don't want
to use a real SomeDependency instance
during tests. A common approach is to either extract an interface
(JUST to make it testable), or to drop the
final
keyword and allow extending the class.Both approaches have their flaws:
- extracting an interface unnecessarily complicates the system, where
only one "real" implementation of an interface is assumed.- dropping the
final
keyword allows for the rabbit-hole of
inheritance abuse, like greatly described in this article:
https://front-line-php.com/object-orientedI believe I came up with a better idea: what if we could leave both
benefits of prohibiting the inheritance abuse and also allow not to
clutter our namespace with excess entities like interfaces? I hereby
suggest to combine the responsibilities of a class and an interface
into one thing like that:final class interface C {} final class D implements C {}
Now other classes can "implement" this class as well. Introduction of
the new syntax (class interface
) also solves BC problem - if you
want to forbid your classes to be reimplemented whatsoever, you can
still stick to thefinal class
syntax. Although it is also possible
to allow "reimplementing" ANY class, then new syntax is not needed -
but some library writers like Marco Pivetta could be sad about that I
suppose.Soo..., what do you think? Could this be a valuable addition to the
language?Regards,
Illia / someniatko--
To unsubscribe, visit: https://www.php.net/unsub.php
Although I undeterstand and agree with the annoyance of both limitations
presented, I think one important factor that makes this an issue lies with
PSR-4 and autoloading. If you could declare the interface and the
implementation in the same file, as you do with Typescript, the drawback of
extracting an interface for testing would be greatly diminished.
Unfortunately undoing PSR-4 shortcomings and getting something baked into
Composer or PHP to address this seems fairly challenging.
Hi again, internals
My marathon of some crazy ideas continues :D, with less crazy one this time.
Idea
Allow "reimplementing" the non-static public API (that is public
properties and methods, excluding constructor) of a class by other
classes like this:final class interface A { public string $s; public function __construct(string $s) { $this->s = $s; } public static function fromInt(int $i): self { return new self((string) $i); } public function foo(): int { return 42; } } final class B implements A { public string $s = 'hello'; public function foo(): int { return 69; } } function usesA(A $param): void {} usesA(new B); // this works
Explanation
Consider there is a class like this:
final class Service { public function __construct(private SomeDependency $dependency) {} // ... } final class SomeDependency { // ... }
We want to write some tests for the Service class, but we don't want
to use a real SomeDependency instance
during tests. A common approach is to either extract an interface
(JUST to make it testable), or to drop the
final
keyword and allow extending the class.Both approaches have their flaws:
- extracting an interface unnecessarily complicates the system, where
only one "real" implementation of an interface is assumed.- dropping the
final
keyword allows for the rabbit-hole of
inheritance abuse, like greatly described in this article:
https://front-line-php.com/object-orientedI believe I came up with a better idea: what if we could leave both
benefits of prohibiting the inheritance abuse and also allow not to
clutter our namespace with excess entities like interfaces? I hereby
suggest to combine the responsibilities of a class and an interface
into one thing like that:final class interface C {} final class D implements C {}
Now other classes can "implement" this class as well. Introduction of
the new syntax (class interface
) also solves BC problem - if you
want to forbid your classes to be reimplemented whatsoever, you can
still stick to thefinal class
syntax. Although it is also possible
to allow "reimplementing" ANY class, then new syntax is not needed -
but some library writers like Marco Pivetta could be sad about that I
suppose.Soo..., what do you think? Could this be a valuable addition to the language?
Regards,
Illia / someniatko--
To unsubscribe, visit: https://www.php.net/unsub.php
This reminds me a lot of C#'s "extension methods" which are quite
useful. If PHP had something like that, you could do some really nice
things:
class BMock {
public static function bar from A(): int { return 69; }
}
$a = new A(123);
echo $a->bar(); // output: 69
class BReal {
public static function bar from A(): int { return $self->foo(); }
}
// error A::bar() already defined on BMock
where from A
is syntax sugar for public function bar(A $self) {}
and trying $a->bar()
is simply syntax sugar for BMock::bar($a);
Hi Someniatko,
Hi again, internals
My marathon of some crazy ideas continues :D, with less crazy one this
time.Idea
Allow "reimplementing" the non-static public API (that is public
properties and methods, excluding constructor) of a class by other
classes like this:final class interface A { public string $s; public function __construct(string $s) { $this->s = $s; } public static function fromInt(int $i): self { return new self((string) $i); } public function foo(): int { return 42; } } final class B implements A { public string $s = 'hello'; public function foo(): int { return 69; } } function usesA(A $param): void {} usesA(new B); // this works
Explanation
Consider there is a class like this:
final class Service { public function __construct(private SomeDependency $dependency) {} // ... } final class SomeDependency { // ... }
We want to write some tests for the Service class, but we don't want
to use a real SomeDependency instance
during tests. A common approach is to either extract an interface
(JUST to make it testable), or to drop the
final
keyword and allow extending the class.Both approaches have their flaws:
- extracting an interface unnecessarily complicates the system, where
only one "real" implementation of an interface is assumed.- dropping the
final
keyword allows for the rabbit-hole of
inheritance abuse, like greatly described in this article:
https://front-line-php.com/object-orientedI believe I came up with a better idea: what if we could leave both
benefits of prohibiting the inheritance abuse and also allow not to
clutter our namespace with excess entities like interfaces? I hereby
suggest to combine the responsibilities of a class and an interface
into one thing like that:final class interface C {} final class D implements C {}
Now other classes can "implement" this class as well. Introduction of
the new syntax (class interface
) also solves BC problem - if you
want to forbid your classes to be reimplemented whatsoever, you can
still stick to thefinal class
syntax. Although it is also possible
to allow "reimplementing" ANY class, then new syntax is not needed -
but some library writers like Marco Pivetta could be sad about that I
suppose.Soo..., what do you think? Could this be a valuable addition to the
language?
This sounds interesting but it breaks some expectations.
Interesting because you can have any class act as an interface for other
classes with the interface being built up of any public properties or
method that exists on that class.
Ok, maybe not any class but just a final class. And also maybe just a final
class that doesn't yet implement an interface.
The expectation it breaks is that if you have a final class, whenever you
use it, you expect some specific implementation exists on it.
And this would break the expectation and in real life someone might pass a
totally different implementation.
Will not break generic LSP but still will break some expectations.
I believe other options should be followed that are more straightforward:
If it's your internal code not shared with others, you can easily extract
an interface at any point where you need to inject another instance (when
testing).
Also, if it's internal code, you can just not mark the class as final.
If it's a library code you use or share with others, a class should not be
marked as final if there is no interface they implement and that should be
fixed by extracting an interface.
If it's a value object, you don't need to mock it, wherever it exists.
Regards,
Alex
This sounds interesting but it breaks some expectations.
Interesting because you can have any class act as an interface for other classes with the interface being built up of any public properties or method that exists on that class.
In my original suggestion, it's not like any class, but only those you
specifically mark as class interface
, not just class
. Closer to
the end of my idea I also suggest that maybe it's worth it to allow
implementing, like interfaces, ANY classes, not only those
specifically marked with the new syntax, but this indeed brings up
some issues.
Ok, maybe not any class but just a final class.
I see no issue there of allowing to reimplement the interface of a
non-final class. This still is better than extending from it IMO. But
we probably should prohibit it from abstract classes.
And also maybe just a final class that doesn't yet implement an interface.
Also, here is no conflict as well. When reimplementing the interface
of the class, you can treat it as a normal interface - which can
extend from other interfaces.
The expectation it breaks is that if you have a final class, whenever you use it, you expect some specific implementation exists on it.
And this would break the expectation and in real life someone might pass a totally different implementation.
Will not break generic LSP but still will break some expectations.
That's why a new syntax is brought up. This expectation might indeed
break if we allow to reimplement any existing final class, instead of
requiring a manual opt-in.
(sorry, this email is sent to you the second time, Alexandru, because
I forgot to reply to the maillist as well)
Regards,
Illia / somenitko
This sounds interesting but it breaks some expectations.
Interesting because you can have any class act as an interface for other
classes with the interface being built up of any public properties or
method that exists on that class.In my original suggestion, it's not like any class, but only those you
specifically mark asclass interface
, not justclass
. Closer to
the end of my idea I also suggest that maybe it's worth it to allow
implementing, like interfaces, ANY classes, not only those
specifically marked with the new syntax, but this indeed brings up
some issues.The expectation it breaks is that if you have a final class, whenever
you use it, you expect some specific implementation exists on it.
And this would break the expectation and in real life someone might pass
a totally different implementation.
Will not break generic LSP but still will break some expectations.That's why a new syntax is brought up. This expectation might indeed
break if we allow to reimplement any existing final class, instead of
requiring a manual opt-in.
Understood, thanks for explaining.
So it's mostly a syntactic sugar that allows defining an interface for the
current class that is already implemented by the current class.
The interface name is the same as the class name and based on the context,
you either use one or the other.
There are some places where this could get in conflict like (new
ReflectionClass(A::class))->isInterface(). But also internally things might
be difficult to sort out to know what to use one or the other.
To have a perfect syntax-sugar, why not also allow the interface name to be
specified?
Something like:
final class interface(ServiceInterface) Service {
public function method1() {}
protected function method2() {}
}
that would generate:
interface ServiceInterface {
public function method1();
}
final class Service implements ServiceInterface {
public function method1() {}
protected function method2() {}
}
As explained previously (and now I got that), autoloading will not work in
this case. But some classmaps can be configured for composer or you could
just force class loading with class_exists()
in limited places where this
is needed (in tests).
Alex
So it's mostly a syntactic sugar that allows defining an interface for
the current class that is already implemented by the current class.
Yeah, while answering to you, I've actually came to the same conclusion.
The interface name is the same as the class name and based on the context,
you either use one or the other.
I think we should distinguish this thing from an interface though.
There are some places where this could get in conflict like (new
ReflectionClass(A::class))->isInterface(). But also internally things might
be difficult to sort out to know what to use one or the other.
Yeah, I also thought about reflection, exactly about this method. I believe
it's better not to lie, and make isInterface() return false. However,
another method should be added, e.g. isClassInterface() - which will return
true only in case of class interface
, or, maybe a more general one like
isImplementable(), which will also return true for interfaces.
To have a perfect syntax-sugar, why not also allow the interface name to
be specified?
Something like:
final class interface(ServiceInterface) Service {
public function method1() {}
protected function method2() {}
}that would generate:
interface ServiceInterface {
public function method1();
}
final class Service implements ServiceInterface {
public function method1() {}
protected function method2() {}
}
Well that goes against my initial concern of not introducing additional
entities to the system. However it indeed reduces the amount of code to
write and is more "transparent" type of syntaxic sugar, like the promoted
properties in constructors - this feature also translates to PHP code. If
my suggestion ever becomes an RFC, this way of implementing it may become a
separate vote I suppose.
Regards,
Illia / someniatko
Hi again, internals
My marathon of some crazy ideas continues :D, with less crazy one this time.
Idea
Allow "reimplementing" the non-static public API (that is public
properties and methods, excluding constructor) of a class by other
classes like this:final class interface A { public string $s; public function __construct(string $s) { $this->s = $s; } public static function fromInt(int $i): self { return new self((string) $i); } public function foo(): int { return 42; } } final class B implements A { public string $s = 'hello'; public function foo(): int { return 69; } } function usesA(A $param): void {} usesA(new B); // this works
Explanation
Consider there is a class like this:
final class Service { public function __construct(private SomeDependency $dependency) {} // ... } final class SomeDependency { // ... }
We want to write some tests for the Service class, but we don't want
to use a real SomeDependency instance
during tests. A common approach is to either extract an interface
(JUST to make it testable), or to drop the
final
keyword and allow extending the class.Both approaches have their flaws:
- extracting an interface unnecessarily complicates the system, where
only one "real" implementation of an interface is assumed.- dropping the
final
keyword allows for the rabbit-hole of
inheritance abuse, like greatly described in this article:
https://front-line-php.com/object-orientedI believe I came up with a better idea: what if we could leave both
benefits of prohibiting the inheritance abuse and also allow not to
clutter our namespace with excess entities like interfaces? I hereby
suggest to combine the responsibilities of a class and an interface
into one thing like that:final class interface C {} final class D implements C {}
Now other classes can "implement" this class as well. Introduction of
the new syntax (class interface
) also solves BC problem - if you
want to forbid your classes to be reimplemented whatsoever, you can
still stick to thefinal class
syntax. Although it is also possible
to allow "reimplementing" ANY class, then new syntax is not needed -
but some library writers like Marco Pivetta could be sad about that I
suppose.Soo..., what do you think? Could this be a valuable addition to the language?
Regards,
Illia / someniatko--
To unsubscribe, visit: https://www.php.net/unsub.php
Hi
It’s possible I’m missing something here, because it’s hard to tell which parts are new syntax you’re suggesting, and which parts are unfortunate typos…
Can you explain how using a regular interface (or even an abstract base class if you prefer) doesn’t already achieve what you want?
Cheers
Stephen
We want to write some tests for the Service class, but we don't want
to use a real SomeDependency instance
during tests. A common approach is to either extract an interface
(JUST to make it testable), or to drop the
final
keyword and allow extending the class.Both approaches have their flaws:
- extracting an interface unnecessarily complicates the system, where
only one "real" implementation of an interface is assumed.- dropping the
final
keyword allows for the rabbit-hole of
inheritance abuse, like greatly described in this article:
https://front-line-php.com/object-oriented
There's another approach that requires neither: removing final
during class loading, for tests only.
See https://github.com/dg/bypass-finals (and
https://github.com/nunomaduro/mock-final-classes for plug'n'play
integration with PHPUnit).