[PHP-DEV] Static class

On Sun, Jun 16, 2024, at 14:13, Rowan Tommins [IMSoP] wrote:

On 15 June 2024 18:39:20 BST, Valentin Udaltsov <udaltsov.valentin@gmail.com> wrote:

PHP does not have internal/private functions.

Very often I use functions for really simple things like array_flatten

(and register them via composer.json.autoload.files). But when the function

requires decomposition to smaller ones, I switch to a static class with one

public and some private static methods.

Namespace visibility or a native “internal” attribute is something that would be really useful, I think, because it would be able to apply to whole classes as well as functions and constants. It would also allow you to make the refactoring you mention without having to switch all calls from Foo\bar() to Foo::bar()

In general, I think PHP should embrace the concept of “packages” much more - for larger apps, there’s a much greater scope for performance if OpCache could analyse multiple files at once, and make assumptions about them. (A consideration which didn’t apply when autoloading was first introduced.)

With that in mind, instead of function autoloading, maybe we should think about an “include_all” function, which accepted a wildcard/glob pattern. That would make it much easier to use preloading, and move away from the “one class per file” layout, without blessing an official tool like Composer, or a particular file layout.

Put together, those features would provide a powerful alternative to a lot of static classes - i.e. those where no state or inheritance is required, just a grouping of code.

Regards,

Rowan Tommins

[IMSoP]

Considering that I brought up an internal attribute not too long ago and it seems like “packaging” is a giant can of worms, I’d like to suggest we don’t even approach the topic. People on this list have very specific ideas of what that means and unless someone is going to finally try championing an rfc after 30 years, it shouldn’t even be a factor.

— Rob

Regarding function autoloading:

A more interesting question to me is a convention where to put each function.
With classes, PSR-4 tells us exactly what one can expect to find in a
class file with a given name.
Perhaps a separate directory tree per package, with one file per sub-namespace?
Or just a package-wide functions.php?

Regarding static methods vs functions.

From the older thread, I can see different levels of possible
objection to all-static classes:
1. "Every static method should instead be either a non-static method,
or a regular procedural function."
2. "Static methods are acceptable, but no class should exist that has
only static methods."
3. "Static methods and all-static classes are acceptable, but no new
language feature is needed to specifically support all-static
classes."
4. "A new language feature for all-static classes could be acceptable,
but it should be very minimal and declarative."

I see myself somewhere between 3 and 4.

The DX of static methods can be evaluated from two perspectives: The
calling side, and the declaration side.
On the calling code side, the class of a static method call is usually
visible in the place where the call is made,
For a function call, the namespace is usually only visible in the
imports list at the top of the file.
The class name that is part of a static method call hints at an object
type, whereas for a namespace fragment it may not be really clear what
it describes.
In a static factory call, like `Color::fromRgb($red, $green, $blue)`,
I would argue that having the class name as part of the call improves
DX.
On the declaration side, I like to have a static factory in the same
place as the class itself.

For other cases, it can vary whether having a class name as part of
the call improves DX or not.
On the declaration side, static methods do bring some additional
features, that is, to split out some parts into private and protected
methods.
This said, in many cases such classes could be converted to non-static
instantiable classes.

One benefit of static methods is how they can be part of incremental
refactoring.
- Split some functionality into a private method.
- Notice that this method does not depend on any object state, so we
make it static.
- Now that it is static, it could as well be public, if we clean up
the signature a bit. This way it can be reused and tested
independently.
- Now that it is public static, I rather want to move it out of the
class, into a utility class.
- Now that we have this standalone method, we could convert it to a
procedural function.
- OR, we may decide that the entire utility class should actually be
converted to a service, and the method to be not static.

Or:
- Move some functionality out of the constructor into a static factory.
- Let the static factory return different implementations based on the
parameters. E.g. Color::fromRbg() could return a GrayscaleColor object
or a RainbowColor object, depending on parameter values.
- Make `Color` an all-static class, because the specific
implementations have dedicated classes.
- Convert the static methods on `Color` into object methods on
`ColorFromRgbFactory`.
- OR, we discover static polymorphism (*) and "abstract protected
static function".
- Later we might decide that this was awkward, and convert everything
to object methods.

In both of these scenarios we have steps with an all-static class.
There are possibilities to move away from that.
But in some cases the all-static class is actually the sweet spot
where we want to stay.

Another case for an all-static class would be a class that only
contains class constants.
Yes, we can use `define(...)`, but sometimes we want class constants,
for similar arguments as above.

A third case is if the all-static class already exists, as part of a
legacy codebase, and we could not break it up even if we wanted to.

If we do end up with an all-static class, even as an intermediate
state, it can be useful to declare this in some way.
Developers should immediately see whether a class is instantiable or not.

A trait or a base class or an explicit private constructor can do the
job, but it won't be as universal as a language feature.

On the other hand:
From the older proposal by Lanre Waju:

All methods and variables in the class are implicitly static.

I would very much oppose this.
These implicit semantics would make it harder and error-prone to move
methods around from a regular class to an all-static class.
And it would be confusing to look at such a method, because typically
the class-level static declaration won't be visible when you look at
the method.
Also think of the noise in git commits caused by the conversion to an
all-static class.
(Btw the same problem applies to class-level readonly)

To me, the best version of a class-level "static" keyword would be as
a contract, and nothing more:
- The class cannot be instantiated.
- It cannot have a constructor or destructor, or any magic methods
that only apply to objects.
- It can only extend or be extended by other static classes.
(otherwise it should be declared "abstract" instead)

The same could be achieved with a native base class or trait with
private constructor, or even the same thing from php-fig.
This would be less clear and less powerful than a static keyword, but
also less invasive to the language.

(One such language feature could be a language-level base class or
trait, simply for the sake of unification)

From this thread:

In particular: static classes are implied final and they cannot inherit from any other class.

With the version of the static keyword as above, I would be against
this limitation.
Static polymorphism (*) is possible today, and it should be perfectly
fine to slap a "static" keyword on these classes, if they are
all-static.
This is not about whether or not we like or dislike static
polymorphism, but about not attaching unrelated meaning to a keyword.
An explicit "static final class" is more obvious than "static class"
with implicit final.

(*) The term "static polymorphism" is a bit questionable.
I obviously mean this:

class B {
  static function f() {
    return static::g();
  }
  protected static function g() {
    return null;
  }
}

class C extends B {
  protected static function g() {
    return 5;
  }
}

assert(C::f() === 5);

I have used this in the past, and I am now questioning my choices in
some of these cases.
Still I would not consider it an anti-pattern.

--- Andreas

On Sun, Jun 16, 2024, at 10:24 AM, Andreas Hennings wrote:

Regarding function autoloading:

A more interesting question to me is a convention where to put each function.
With classes, PSR-4 tells us exactly what one can expect to find in a
class file with a given name.
Perhaps a separate directory tree per package, with one file per sub-namespace?
Or just a package-wide functions.php?

Regarding static methods vs functions.

From the older thread, I can see different levels of possible
objection to all-static classes:
1. "Every static method should instead be either a non-static method,
or a regular procedural function."
2. "Static methods are acceptable, but no class should exist that has
only static methods."
3. "Static methods and all-static classes are acceptable, but no new
language feature is needed to specifically support all-static
classes."
4. "A new language feature for all-static classes could be acceptable,
but it should be very minimal and declarative."

I see myself somewhere between 3 and 4.

For reference, I have documented my stance on statics here:

--Larry Garfield

On 16/06/2024 16:24, Andreas Hennings wrote:

For a function call, the namespace is usually only visible in the
imports list at the top of the file.

This is one of those interesting cases where the language itself is flexible, but people are used to a specific style: a use statement can import any level of namespace, to provide as much or as little context as needed in a particular piece of code.

namespace Acme\Artistry\Color {
function fromRgb($red, $green, $blue) { /*...*/ }
}

namespace My\Own\Application {
use Acme\Artistry\Color;

 // \.\.\.
 $color = Color\\fromRgb\(1,2,3\);
 // \.\.\.

}

Maybe that's rare in practice, but it's always tricky to judge how much the language should account for such habits - see also "every class needs a new file", and my pet hate "deprecation notices get promoted to errors".

The class name that is part of a static method call hints at an object
type, whereas for a namespace fragment it may not be really clear what
it describes.
In a static factory call, like `Color::fromRgb($red, $green, $blue)`,
I would argue that having the class name as part of the call improves
DX.

This example seems slightly off: if the static method is on the class it is a factory for, that class clearly isn't completely static.

Presumably what we're actually talking about is something like "ColorFactory::fromRgb(...)" - or maybe "GraphicsFactory::colorFromRgb(...)", where you might well want to abbreviate to "colorFromRgb(...)".

Your points are generally well made, I just wanted to point out there are some nuances in that particular area.

Regards,

--
Rowan Tommins
[IMSoP]

Here is a relevant example usage of static classes/polymorphism on my personal site/app:
https://gist.github.com/oplanre/6894f56fb61134ee5ea93cf262ba3662

On Sun, Jun 16, 2024 at 10:38 AM Andreas Hennings <andreas@dqxtech.net> wrote:

Regarding function autoloading:

A more interesting question to me is a convention where to put each function.
With classes, PSR-4 tells us exactly what one can expect to find in a
class file with a given name.
Perhaps a separate directory tree per package, with one file per sub-namespace?
Or just a package-wide functions.php?

Regarding static methods vs functions.

From the older thread, I can see different levels of possible
objection to all-static classes:

  1. “Every static method should instead be either a non-static method,
    or a regular procedural function.”
  2. “Static methods are acceptable, but no class should exist that has
    only static methods.”
  3. “Static methods and all-static classes are acceptable, but no new
    language feature is needed to specifically support all-static
    classes.”
  4. “A new language feature for all-static classes could be acceptable,
    but it should be very minimal and declarative.”

I see myself somewhere between 3 and 4.

The DX of static methods can be evaluated from two perspectives: The
calling side, and the declaration side.
On the calling code side, the class of a static method call is usually
visible in the place where the call is made,
For a function call, the namespace is usually only visible in the
imports list at the top of the file.
The class name that is part of a static method call hints at an object
type, whereas for a namespace fragment it may not be really clear what
it describes.
In a static factory call, like Color::fromRgb($red, $green, $blue),
I would argue that having the class name as part of the call improves
DX.
On the declaration side, I like to have a static factory in the same
place as the class itself.

For other cases, it can vary whether having a class name as part of
the call improves DX or not.
On the declaration side, static methods do bring some additional
features, that is, to split out some parts into private and protected
methods.
This said, in many cases such classes could be converted to non-static
instantiable classes.

One benefit of static methods is how they can be part of incremental
refactoring.

  • Split some functionality into a private method.
  • Notice that this method does not depend on any object state, so we
    make it static.
  • Now that it is static, it could as well be public, if we clean up
    the signature a bit. This way it can be reused and tested
    independently.
  • Now that it is public static, I rather want to move it out of the
    class, into a utility class.
  • Now that we have this standalone method, we could convert it to a
    procedural function.
  • OR, we may decide that the entire utility class should actually be
    converted to a service, and the method to be not static.

Or:

  • Move some functionality out of the constructor into a static factory.
  • Let the static factory return different implementations based on the
    parameters. E.g. Color::fromRbg() could return a GrayscaleColor object
    or a RainbowColor object, depending on parameter values.
  • Make Color an all-static class, because the specific
    implementations have dedicated classes.
  • Convert the static methods on Color into object methods on
    ColorFromRgbFactory.
  • OR, we discover static polymorphism (*) and “abstract protected
    static function”.
  • Later we might decide that this was awkward, and convert everything
    to object methods.

In both of these scenarios we have steps with an all-static class.
There are possibilities to move away from that.
But in some cases the all-static class is actually the sweet spot
where we want to stay.

Another case for an all-static class would be a class that only
contains class constants.
Yes, we can use define(...), but sometimes we want class constants,
for similar arguments as above.

A third case is if the all-static class already exists, as part of a
legacy codebase, and we could not break it up even if we wanted to.

If we do end up with an all-static class, even as an intermediate
state, it can be useful to declare this in some way.
Developers should immediately see whether a class is instantiable or not.

A trait or a base class or an explicit private constructor can do the
job, but it won’t be as universal as a language feature.

On the other hand:
From the older proposal by Lanre Waju:

All methods and variables in the class are implicitly static.

I would very much oppose this.
These implicit semantics would make it harder and error-prone to move
methods around from a regular class to an all-static class.
And it would be confusing to look at such a method, because typically
the class-level static declaration won’t be visible when you look at
the method.
Also think of the noise in git commits caused by the conversion to an
all-static class.
(Btw the same problem applies to class-level readonly)

To me, the best version of a class-level “static” keyword would be as
a contract, and nothing more:

  • The class cannot be instantiated.
  • It cannot have a constructor or destructor, or any magic methods
    that only apply to objects.
  • It can only extend or be extended by other static classes.
    (otherwise it should be declared “abstract” instead)

The same could be achieved with a native base class or trait with
private constructor, or even the same thing from php-fig.
This would be less clear and less powerful than a static keyword, but
also less invasive to the language.

(One such language feature could be a language-level base class or
trait, simply for the sake of unification)

From this thread:

In particular: static classes are implied final and they cannot inherit from any other class.

With the version of the static keyword as above, I would be against
this limitation.
Static polymorphism (*) is possible today, and it should be perfectly
fine to slap a “static” keyword on these classes, if they are
all-static.
This is not about whether or not we like or dislike static
polymorphism, but about not attaching unrelated meaning to a keyword.
An explicit “static final class” is more obvious than “static class”
with implicit final.

(*) The term “static polymorphism” is a bit questionable.
I obviously mean this:

class B {
static function f() {
return static::g();
}
protected static function g() {
return null;
}
}

class C extends B {
protected static function g() {
return 5;
}
}

assert(C::f() === 5);

I have used this in the past, and I am now questioning my choices in
some of these cases.
Still I would not consider it an anti-pattern.

— Andreas

On Sun, 16 Jun 2024 at 19:09, Rowan Tommins [IMSoP]
<imsop.php@rwec.co.uk> wrote:

On 16/06/2024 16:24, Andreas Hennings wrote:

For a function call, the namespace is usually only visible in the
imports list at the top of the file.

This is one of those interesting cases where the language itself is flexible, but people are used to a specific style: a use statement can import any level of namespace, to provide as much or as little context as needed in a particular piece of code.

namespace Acme\Artistry\Color {
    function fromRgb($red, $green, $blue) { /*...*/ }
}

namespace My\Own\Application {
    use Acme\Artistry\Color;

    // ...
    $color = Color\fromRgb(1,2,3);
    // ...
}

Maybe that's rare in practice, but it's always tricky to judge how much the language should account for such habits - see also "every class needs a new file", and my pet hate "deprecation notices get promoted to errors".

Yes, the possibility for namespace part imports exists.
Unfortunately these namespace imports usually need to be set up
manually, whereas the regular class or function import has better
automatic support from the IDE.
Also, namespace fragments are often more generic than function names,
so more likely to need a custom alias.

The class name that is part of a static method call hints at an object
type, whereas for a namespace fragment it may not be really clear what
it describes.
In a static factory call, like `Color::fromRgb($red, $green, $blue)`,
I would argue that having the class name as part of the call improves
DX.

This example seems slightly off: if the static method is on the class it is a factory for, that class clearly isn't completely static.

There are two separate argument tracks here.
The `Color::fromRgb($red, $green, $blue)` has a nice DX, and justifies
the eixstence of static methods in general.
This also aligns with Larry's argument about "named constructors", or
the more general "type context".

Then, you may rename the instantiable part of the class, while leaving
the static factories in place.
You end up with an all-static class.

Or you already start with different implementation classes like
RgbColor and HexColor, all implementing ColorInterface, but you want
one centralized place for static factories.
I personally prefer to call this `Color` instead of `ColorFactory`.
For me `ColorFactory::create()` would give me a ColorFactoryInterface
object, not a ColorInterface object.

Presumably what we're actually talking about is something like "ColorFactory::fromRgb(...)" - or maybe "GraphicsFactory::colorFromRgb(...)",

Basically yes.

where you might well want to abbreviate to "colorFromRgb(...)".

I assume you mean as a procedural function?
Technically that would be possible.
But I still like to keep the `Color::` part which is suggestive of the
`ColorInterface`, even when the class `Color` itself is not really
instantiable.
A namespace fragment or a function prefix is not as suggestive that
this produces a ColorInterface object

Your points are generally well made, I just wanted to point out there are some nuances in that particular area.

Thanks.

On Sun, 16 Jun 2024 at 17:44, Larry Garfield <larry@garfieldtech.com> wrote:

On Sun, Jun 16, 2024, at 10:24 AM, Andreas Hennings wrote:
> Regarding function autoloading:
>
> A more interesting question to me is a convention where to put each function.
> With classes, PSR-4 tells us exactly what one can expect to find in a
> class file with a given name.
> Perhaps a separate directory tree per package, with one file per sub-namespace?
> Or just a package-wide functions.php?
>
>
> Regarding static methods vs functions.
>
> From the older thread, I can see different levels of possible
> objection to all-static classes:
> 1. "Every static method should instead be either a non-static method,
> or a regular procedural function."
> 2. "Static methods are acceptable, but no class should exist that has
> only static methods."
> 3. "Static methods and all-static classes are acceptable, but no new
> language feature is needed to specifically support all-static
> classes."
> 4. "A new language feature for all-static classes could be acceptable,
> but it should be very minimal and declarative."
>
> I see myself somewhere between 3 and 4.

For reference, I have documented my stance on statics here:

Cutting through the static | PeakD

--Larry Garfield

Thanks, Larry!
This aligns more or less with your messages from the older thread.

Make it a function.

This is fine, but:
If you would replace all of your functions with static methods in
all-static classes, your code would still be as testable as before.
The calls would look more verbose, perhaps more confusing.

Developers may find reasons to use static methods and possibly
all-static classes over functions:
- Conformity with an existing code base or ecosystem.
- Lack of a convention on where to put your namespaced methods, and
which namespace to use.
- Personal habit and familiarity.
- Convenience of moving code between static and non-static methods.
- The possibility of using private and protected static methods, and
static polymorphism (whether that is good or bad).
- Easy access to class constants.
- The static methods already exist.

Besides, the namespace lookup is generally less ambiguous for classes
than it is for methods.
A `str_starts_with()` call could reference a function
`Current\Name\Space\str_starts_with()`, or the global
`\str_starts_with()`.
A class always needs the explicit alias or a `\\` prefix.
At the same time, the top-level namespace is crowded with native
procedural functions, but not so much with native classes.
At first, this is just an inconvenience when reading the code.
But it can also lead to real bugs, if a function alias gets lost when
you resolve a merge conflict, and you don't notice it because there is
a top-level function with the same name.

So, the above would be reasons why developers might not go all the way
to namespaced procedural functions.

Then, the mere presence and habit of static functions will lead to
some cases of all-static classes.

Even if you only use static methods as named constructors (static
factory method), you can end up with an all-static class, as in my
example above:
- You start with private constructor + public static factory.
- You rename the part of the class that is instantiable, to better
distinguish different implementations, but you keep the static methods
in place for BC reasons.
  (this assumes that all dependent packages depend on the static
factory and on the interface, but not on the class directly).
- Now it would be nice to visibly mark the class non instantiable.

-- Andreas

On 16/06/2024 18:54, Andreas Hennings wrote:

Yes, the possibility for namespace part imports exists.
Unfortunately these namespace imports usually need to be set up
manually, whereas the regular class or function import has better
automatic support from the IDE.
Also, namespace fragments are often more generic than function names,
so more likely to need a custom alias.

That's why I said it was tricky: the language doesn't prevent IDEs suggesting different imports; nor does it require you to name namespace fragments differently from classes; but that's what happens in practice. So to what extent do we build the language based on how it *can* be used, and to what extent do we build it based on how it *is* used? What if that usage changes over time? What if we add more ways to do it, and those aren't used either? I don't think there's an easy answer.

--
Rowan Tommins
[IMSoP]

On Sun, 16 Jun 2024 at 20:08, Rowan Tommins [IMSoP]
<imsop.php@rwec.co.uk> wrote:

On 16/06/2024 18:54, Andreas Hennings wrote:

Yes, the possibility for namespace part imports exists.
Unfortunately these namespace imports usually need to be set up
manually, whereas the regular class or function import has better
automatic support from the IDE.
Also, namespace fragments are often more generic than function names,
so more likely to need a custom alias.

That's why I said it was tricky: the language doesn't prevent IDEs suggesting different imports; nor does it require you to name namespace fragments differently from classes; but that's what happens in practice.

Right.
The main thing here would be to have a 90% deterministic way to
generate the imports.
We want to avoid meaningless choices a developer needs to make case by case.
I see some difficulties here, but there are probably ways to handle it.

So to what extent do we build the language based on how it *can* be used, and to what extent do we build it based on how it *is* used? What if that usage changes over time? What if we add more ways to do it, and those aren't used either? I don't think there's an easy answer.

It depends how disruptive the change is.

E.g. symfony has a Yaml class with only class constants and static methods.

We can argue whether this is good or bad.
Or we can just slap a "static" modifier on the class, so that
developers know what to expect.
This does not prevent future refactoring, where these methods might be
converted to functions.

Btw, even with only the class constants left, this would still be an
all-static class.

On 16/06/2024 14:32, Mike Schinkel wrote:

    Not supporting all features used in the wild would result in
    developers not being able to use static classes for new and/or
    refactored code if they believe they need those excluded features.
    This would stop developers from signaling their intent to readers
    of their code and would stop developers from being able to rely on
    reflection to discern static classes for new and/or refactored
    code when that otherwise could have been possible.

    Disabling existing features just for static classes would result
    in creating two forms of static classes — explicit and implicit —
    and that IMO would be a miss for the addition of static classes.

You wrote the perfect words. Even if I do not agree with all of your six conclusions following, I agree with this preface 100%, to such an extent that I think it should form the underpinning basis for all further decisions on this RFC and be enshrined in the preamble of said RFC, if indeed I ever acquire such permissions as are necessary to actually write it.

The idea of "implicit" versus "explicit" static classes is a clear and powerful one that I think we can all understand, such that a successful RFC would collapse this distinction; any failure to do so would undermine the value of the feature entirely. As a concrete example, then, prohibiting state in a static class is now completely off the table simply because regular classes already support this feature. Whilst some people have talked about idealogical aspirations for this feature which sound valid in a vacuum, we must primarily concern ourselves with the fact that this feature is being introduced into an existing language, not a brand new language where such ideals would have merit. And that is why this statement is so powerful: most (but perhaps not all) of the answers to these disputed questions follow naturally from the clear and guiding principle of this preface.

Thank you so much, Mike, for this! I feel confident I can write this RFC now, and with help from Lanre, we already have the beginnings of an implementation, too!

Cheers,
Bilge

On 17/06/2024 13:49, Bilge wrote:

On 16/06/2024 14:32, Mike Schinkel wrote:

    Not supporting all features used in the wild would result in
    developers not being able to use static classes for new and/or
    refactored code if they believe they need those excluded
    features. This would stop developers from signaling their intent
    to readers of their code and would stop developers from being
    able to rely on reflection to discern static classes for new
    and/or refactored code when that otherwise could have been possible.

    Disabling existing features just for static classes would result
    in creating two forms of static classes — explicit and implicit —
    and that IMO would be a miss for the addition of static classes.

You wrote the perfect words. Even if I do not agree with all of your six conclusions following, I agree with this preface 100%, to such an extent that I think it should form the underpinning basis for all further decisions on this RFC and be enshrined in the preamble of said RFC, if indeed I ever acquire such permissions as are necessary to actually write it.

The idea of "implicit" versus "explicit" static classes is a clear and powerful one that I think we can all understand, such that a successful RFC would collapse this distinction; any failure to do so would undermine the value of the feature entirely. As a concrete example, then, prohibiting state in a static class is now completely off the table simply because regular classes already support this feature. Whilst some people have talked about idealogical aspirations for this feature which sound valid in a vacuum, we must primarily concern ourselves with the fact that this feature is being introduced into an existing language, not a brand new language where such ideals would have merit. And that is why this statement is so powerful: most (but perhaps not all) of the answers to these disputed questions follow naturally from the clear and guiding principle of this preface.

Thank you so much, Mike, for this! I feel confident I can write this RFC now, and with help from Lanre, we already have the beginnings of an implementation, too!

Cheers,
Bilge

Furthermore, for absolute clarity and the avoidance of doubt, I believe this guiding principle can be enshrined on the following single sentence:

 &quot;We cannot remove features from a static class that would otherwise be present in a standard class\.&quot;

I believe following this principle we will arrive at the best implementation of static classes.

Cheers,
Bilge

On Jun 17, 2024, at 8:54 AM, Bilge <bilge@scriptfusion.com> wrote:

Furthermore, for absolute clarity and the avoidance of doubt, I believe this guiding principle can be enshrined on the following single sentence:

“We cannot remove features from a static class that would otherwise be present in a standard class.”

I believe following this principle we will arrive at the best implementation of static classes

A great principle, yes, but needing a small caveat:

“…excepting features that by-nature are not relevant to the static class use-case, such as instance-based magic methods like __get(), __set()`` and __construct()`.”

That might have been implied, but good to make it explicit.

-Mike