Video summary
Navigating a complex codebase is often compared to exploring a maze with an incomplete map, where every new feature or assumption adds a potential dead end or deceptive signpost that increases cognitive load. The core subject of this presentation is the "closed by default" principle, which advocates for minimizing unnecessary assumptions about how code behaves until there is a specific reason to expose them. By starting with classes that are final and keeping properties private, developers can significantly reduce the number of paths their brains must explore, thereby making the codebase more predictable and easier to understand. This approach transforms the mental maze by sealing off irrelevant routes, ensuring that every open path in the code serves a deliberate purpose rather than being an accidental complication.
The speaker demonstrates how native PHP features introduced across various versions can be leveraged to enforce this principle effectively. Starting from PHP 5 with access modifiers and final classes, the presentation progresses through PHP 7's scalar type hints and strict types, PHP 8's typed properties and constructor promotion, up to PHP 8.1's read-only properties and PHP 8.2's read-only classes. Each version offers new tools to eliminate assumptions about data mutability, visibility, and return types. For instance, marking a class as final prevents inheritance, while making properties private or read-only restricts external modification, collectively reducing the cognitive surface area required to understand the software's logic.
To maintain consistency and automate these best practices, the speaker recommends integrating static analysis tools like Rector, PHP CS Fixer, and PHPStan into the development workflow. These tools can automatically refactor code to add missing types, enforce strict typing directives, convert protected members to private, and mark classes as final where appropriate. The synergy between these tools creates a self-reinforcing loop where stricter code allows for more accurate static analysis, which in turn enforces even stricter standards. Furthermore, the presentation clarifies that the closed by default principle complements rather than contradicts the Open/Closed Principle; it simply suggests that software entities should only be open for extension when a genuine need arises, aligning with the YAGNI (You Ain't Gonna Need It) philosophy to avoid over-engineering.
In conclusion, while codebases will always contain some level of complexity, developers have the responsibility to make them as navigable as possible by being intentional about where and when they open new paths in their code. By utilizing language constructs, immutable objects like Value Objects, and automated static analysis rules, teams can reduce opportunities for misuse and create a unified understanding of the system's architecture. This disciplined approach not only improves code quality but also provides a robust framework that guides AI-generated code, ensuring that even as technology evolves, the fundamental goal remains to strip away unnecessary assumptions and reveal a straighter path to understanding.
Read the full video transcript
Please welcome our last speaker for PHP
UK 2026, Yanek Chanel.
Navigating a codebase is like exploring
a maze with a partial map. Initially,
directions are clear,
but the deeper you go into the maze, the
harder it is to find your way around.
you realize that some of the path are
dead ends
and that some of the signposts are
deceptive.
>> As we build our softwares uh we
constantly open new path in our code um
increasing the complexity of the code in
the process. Um most of the time that's
good and necessary but sometimes we
accidentally or even intentionally open
new path that lead nowhere or leave
signposts which are like mental traps,
right?
And every time we do this, we
unnecessarily increase our own cognitive
load and that of our co-workers.
And this is where the close by default
principle comes in. H it can be roughly
summarized this way. So the closed by
default principle removes dead ends from
your mental maze of code revealing a
straighter path to understanding
which kind of sounds like it um
addresses the issue I've just described,
right? But what does this mean in
practice? So if we take this code for
instance, um it's a fairly simple class
called dog. It's got a sound constant
for the sound that dogs make. Um it's
got a couple of uh public properties,
dollar name and the microchip ID which
can be initialized through the
constructor. It's got a bark method
which essentially echoes the uh constant
and a fetch method which makes the dog
bark and return whatever item was
thrown. Um so yeah not much to it and
admittedly a little bit silly but what
uh what's important here is the number
of assumptions that we can extract from
this code. So when you come across this
class, this is what you may assume.
That dog could have child classes. That
sound could be of any type.
That sound could be read from the
outside.
That dollar name and dollar microchip ID
could be of any type. That these
properties could be read from the
outside.
That the values of these properties
could change during execution.
That those values could be changed from
the outside.
that dollar name and dollar microchip ID
could be passed any type in the
constructor
that bark and fetch could be called from
the outside that these methods um could
return anything and finally that dollar
item could be passed any type in fetch's
signature
every single one of these assumptions is
like a different path in your mental
maze of code right and like in the maze
the only way to know for sure if any of
this path leads anywhere is to go down
the path and check for yourself.
Now, some of these assumptions might
actually be necessary, right? But all of
them that's very unlikely. So, what we
are going to do now, we are going to uh
try and eliminate as many assumptions as
possible and we are going to use native
PHP features to do that. We're going to
start all the way back from uh PHP 5 and
see what each new version introduced
that can help us um seal off those path
that we don't need. Right?
So let's start with PHP5. Um PHP5 was a
gamecher when it comes to
object-oriented programming. Um it
introduced things like constructors for
instance, but it also introduced the
public protected and private access
modifiers. Uh so for methods and
properties,
it also introduced final classes. So to
make sure that a class cannot have
children
and class and interface type hinting.
And what does that mean for our code? It
means that we can mark the dog class as
final. So to make sure it can't have any
children, we can mark the dollar name
and dollar ID properties as private.
We can also mark the bark method as
private. Um we could also potentially
mark the fetch method as private, but um
the action of fetching is usually
triggered from the outside, right? So
this one probably makes sense to leave
it as public. Um, what we can do,
however, is specify a type for dollar
item in fetch's signature, which is now
an object of type B.
And what does that mean for our
assumptions?
It means that we can get rid of this one
because dog is now final. So, it can't
have any children anymore.
We can get rid of this one because
dollar name and dollar microchip ID are
now private. So, they can't be read from
the outside anymore.
Because they are private, the values
cannot be changed from the outside any
longer.
Bark is also now private. So it can't be
called from the outside anymore.
And finally, dollar item now has to be
of type ball in fetches signature.
All right, so let's jump ahead and go
straight to PHP 7. PHP7 introduced
scallet type hints and return type
declarations.
back to our code. That means that we can
give a type to dollar name in the
constructor signature, which is now a
string. We could potentially give a type
to dollar microchip ID in the
constructor signature as well, but not
all dogs have a microchip, right? So,
ideally, this one should be nullable,
which we can't do at the moment. So, I'm
just going to leave it as is for now.
What we can do, however, is h specify a
written type for fetch, which is now an
object of type ball.
Back to our assumptions,
we can get rid of this one at the very
bottom because fetch is now has to
return an object of type ball.
And how about this one? Dollar name
could be of any type. Technically, right
now that's not true because through the
constructor, dollar name has to be a
string, but the property itself isn't
typed yet. So, I'm not comfortable
getting rid of this assumption just yet.
How about this one? dollar name could be
passed any type in the constructor.
Surely that's not true anymore, right?
Because it now has to be a string. Well,
as we all know, PHP is a rather
permissive language and by default um if
you pass anything else but a string for
dollar name in the constructor, behind
the scenes, PHP will try to cast it as a
string. But PHP 7 also introduced the
declare statement and the strict types
declaration.
If you stick that directive at the top
of uh your PHP file for all of the
method calls made from within this PHP
file, PHP won't try to cast the wrong
type anymore. And it's an important
distinction to make because having this
directive in this file alone won't
prevent other PHP files from calling
Doug methods with the wrong types.
But if I were to do that for instance,
and so I added a line at the very
bottom, which you can probably can't
see, but it's instantiating the dog
class with an integer for the LA name
instead of a string. This would fail
with a type error exception because of
the presence of the declar types
directive, right?
And what does that mean for assumptions?
Uh, it means we can safely get rid of
this one provided that all of your PHP
files have the uh declares directive.
So, I'm cheating a little bit here, but
I'm going to show you later how to
enforce the presence of this directive
in all of your files um automatically.
All right, PHP 7.1
introduced class constant visibility
and nullable and void types.
What does that mean for our code? It
means that we can mark the sound
constant as private. We can finally give
a proper type to dollar microchip ID in
the constructor signature which is now a
nullable integer.
And we can specify a return type for
bark which is now void.
Back to our assumptions.
That means we can get rid of this one
because sound is now private. So it
can't be accessed from read from the
outside anymore.
How about this one? Dollar microchip ID
could be of any type. It's the same as
dollar name really. Um, at the moment
through the constructor, it has to be a
nullable integer, but the property
itself isn't typed yet. So again, not
comfortable getting rid of that one just
yet.
We can get rid of this one though
because the microchip ID now has to be a
nable integer in the constructor
signature.
And B now has the um void return type.
So it can't return anything anymore.
HP 7.4 four introduced typed properties,
meaning we can finally give a proper
type to dollar name and dollar microchip
ID,
which means we can now safely get rid of
this assumption.
PHP 8 introduced constructor property
promotion. All right, so this one isn't
part of the close by default principle
uh strictly speaking, but it does
simplify the code to some extent. So it
does um reduce um the cognitive load to
some extent, right? But this as um this
feature won't allow us to get rid of any
more assumptions.
HP 8.1
introduced read only property. So a read
only property is a property whose value
cannot change during execution once it's
been set. Right?
So what I've done here is I marked
dollar microchip ID as read only um
because I guess we don't really have a
use case for changing the microchip once
it's been set. But what I've done for
dollar name is I changed it from private
to public read only. Meaning its value
can now be read from the outside but its
value cannot change during execution.
Which kind of makes sense, right?
Because you probably want the name to be
available from the outside, right?
That's not really an information that we
want to hide.
Back to assumptions. That means we can
get rid of this one because dollar name
and dollar microchip ID are now read
only. So the values can't change during
execution anymore. But what we've done
here is we've reintroduced another
assumption which is that dollar name
could be read from the outside. And this
is fine because we actually want that.
PHP2
introduced readonly classes which I
guess was the logical next step after
readonly properties.
Because all of our properties are
already marked as readonly, we can mark
the entire class as readonly instead. Um
again this won't allow us to get rid of
any more assumptions but it does
simplify the code to some exit.
HPA.3
introduced typed class constants
meaning we can give a type to sound
which is now a string
meaning we can get rid of this
assumption.
All right so this was a lot. So um let's
take a quick step back to see how far
we've gone. So we started off with all
of these assumptions on the left hand
side and we ended up with those two
assumptions on the right hand side and
these are fine because these are
intentional.
All right. So very briefly a bunch of
other uh native PHP features which are
relevant for the close by default
principle but which I haven't covered in
the previous examples. I'm not actually
going to go through them because it
would take way too long. But what this
shows really is um how much PHP uh
changes and evolves over time, right? Um
all of these features directly or
indirectly give you more control over
your code, right? They make it more uh
predictable. They make it more
intentional. um they really help you
reduce the cognitive surface of your
code a little bit like you would reduce
the attack surface of your code but from
a security perspective
right so we've just seen some of the
many ways that PHP gives us to um seal
off those path that we don't need right
but keeping all of these rules in mind
while you are writing code yourself or
while you are reviewing somebody else's
code is quite a heavy mental burden in
and of itself, right? But thankfully,
pretty much everything I talked about so
far can be automated through static
analysis. Um, if you're not familiar
with static analysis, uh, in a nutshell,
this is what we are talking about. We
are talking about checking the code
without running it. Um, we're talking
about tools that will inspect your
source code in search of bugs and all
sorts of violations. Uh, and in some
cases will automatically fix those
violations. and all of that without
actually executing anything.
So I'm going to talk about three tools
today. Um the first one is recctor which
is amazing for automated refactors. The
second one is PHP code standards fixer
or PHP CSS fixer which is great to
enforce code styling rules. And the last
one is PHP stand which is amazing to
report all sorts of um bugs and type
errors in your code. So what you would
typically do is you would run the first
two ones first to try and fix as many
errors as possible automatically and
then you would run PHP stand to report
any remaining issues so you can address
those manually. Right?
Uh there are other tools out there but
these ones work really really well and
they are quite complimentary as well. Uh
and they work really well especially
when it comes to the close by default
principle.
All right so let's start with recctor.
Uh, Recctor has an entire rule set uh
dedicated to missing typeins, property
types, constant types, and return types.
Um, all of the rules contained within
these rule sets basically um try to
infer those missing types through all
sorts of clever ways.
Um, so yeah, my recommendation is to use
the um the set list um the rule set
straight away. Uh but what you can do
instead if you prefer is you can go
through the rules contained within the
rule set and kind of like cherrypick the
ones you want to apply to your code.
Another one is readonly property
recctor. So this one will automatically
mark as read only your properties where
it makes sense. And this rule is part of
the PHP 8.1 rule set which you can apply
instead if you'd like recctor to try and
upgrade your codebase to use PHP 8.1
features. Um, as an aside, Recctor has
rule sets for pretty much every PHP
version. Uh, so what you could do, for
instance, is apply this one, level set
list up to PHP 85, which would get
Recctor to upgrade your codebase to use
um PHP 8.5 features.
Uh, what this does uh under the hood
basically is Recctor goes through the
rule sets of every PHP version one by
one up to PHP 8.5. Uh, which is pretty
cool. Anyway, um another one is this one
readonly class vector. So this one will
mark your classes as read only this time
where relevant. It only works for final
classes though because um if you mark a
class as readon, you also potent
potentially affect its children. So that
would be considered unsafe. And this
rule is part of the PHP 8.2 rule set.
Another one is this one save declare
strict types recctor. Um, so this one
will automatically add the declare
strict types directive to your PHP files
that don't have it yet and for which it
is safe to do so because if you add this
directive to a file which is not type
safe already, um, you might just break
the logic of your code essentially. So
this rule makes sure that this directive
is only added to files which are already
type safe. Um, I did leave a small um,
test tube emoji there because this rule
is actually very recent. Uh, I think it
was released a couple of weeks ago. Uh,
so it's still technically considered um,
what's the word? Not really.
Experimental. Thank you very much.
Uh, right. I saved the last one. Uh,
well, the spiciest one for last. Um,
Recctor has this tool called Swiss
knife, which is a tool that to, okay,
which is a tool which they used to use
internally, but which they made public
at some point. Uh the tool has a bunch
of available commands but one of them is
this one finalize classes uh which as
the name indicates will automatically
mark your classes as final where it
makes sense. Now I know there is a
heated debate around the final keyword
out there or they used to be at some
point anyway. Um I will actually get
back to that but for now one of the
common arguments uh against using the
final keyword for uh for classes is that
then you cannot mock the corresponding
classes in your test anymore. Right?
This command actually has an option to
automatically skip the classes which are
already mocked in your test. But instead
of using that my recommendation is to
use this other tool called bipus finals
which is not maintained by recctor this
one. But yeah, if you install this tool
as a um development composer dependency
and you stick that line somewhere, so by
finals enable, if you stick that line
somewhere in your test configuration,
your test will then completely ignore
the final keyword for classes. Um
meaning you can mark any class you want
as final. This won't affect your test.
Um so yeah, my recommendation is to use
those two tools together because they
work really well.
All right, let's move on to PHP CSS
fixer. Just a couple of rules here. Um,
the first one is this one, modifier
keywords, uh, which will basically
enforce the use of access modifiers in
your code. Uh, so the public protected
and private keywords.
This rule is part of the PERCS rule set.
Uh, so for those who don't know, per is
a code styling specification which
replaced PSR12 a few years ago. Uh so
yeah, my recommendation is to apply the
rule set straight away because it makes
a lot of u sensible decisions for you
and it includes the modifier keywords
rule by default.
Another one is this one protected to
private which will automatically mark as
private uh all of your um methods and
properties which were unnecessarily
marked as protected so far. Um so yeah,
it's protected to private. It's not
public to private, but it's quite quite
useful already.
Um, and lastly, PHP stand. Um, so, well,
there was not like a talk about PHP
stand this morning, but if you missed
that and if you're not familiar with PHP
stand, PHP stand basically has um 11
levels of strictness, right? Um, the
lowest level being level zero, which is
also the level by default, and the
strictest level being level 10. If you
set PHP stand at level six in your
application, it will start reporting any
missing type hints, property types, and
return types. It doesn't cover constant
types for some reason. But for that, you
can use this PHP stand extension called
type coverage, which is maintained by
Thomas Vuba, who also happens to be the
main author of Recctor. Um so yeah if
you install this extension and you set
its constant setting to 100 so for 100%
PHP stand will start um reporting any
missing constant types as well.
Uh technically the extension also covers
missing type hints property types and
return types that's already covered by
PHP stand. So yeah there is a little bit
of overlap there. Um the main difference
really is the uh the format of the
output.
There's actually something interesting
to note here because
the more types you fix by following PHP
stands report the more of your files
become type safe right meaning the next
time you run uh recctor recctor will be
able to add the declare strict types
directive to more and more files right
and what this highlights really is that
the stricter your code is the better
static analysis becomes uh and it's some
sort of self-reinforcing feedback loop
because then the better static analysis
is, the stricter your code becomes in
turn.
Last thing about PHP stand, uh, PHP
stand can help us um, reduce the
cognitive surface of our codes in ways
that go beyond what PHP offers natively.
Um, I'm only going to mention a couple
of features uh, in passing today, but I
really invite you to to check them out
if you're not familiar with them
already. The first one is the fact that
PHP stand supports all sorts of um more
specific types through annotations and
you will find the list of those uh of
the supported annotations in this
article.
And the other one is uh generics. So
generics are an active topic of
conversation in the PHP community, but
as far as I'm aware, support for
generics isn't planned for a specific
future PHP version yet. But in the
meantime, hello. In the meantime, you
can use PHP stand to get support for
generics.
If you're not familiar with generics, I
invite you to check this article,
Generics by examples, because it will
really um it will help you wrap your
head around the concept of generics,
which is a very useful concept. So,
yeah, I really invite you to check it
out if you're not familiar already.
All right, so this slide is a little bit
of a disclaimer slide. Um the closed by
default principle isn't anything new
really. It's more of a uh a reframing of
existing principles which are more or
less related to it. So I'm going to
mention a few quickly. Uh the first one
is encapsulation. Obviously very closely
related this one. Um yagn is another
one. You ain't going to need it. Um
immutability is a big one as well
because if you've got an object and you
know that the values of this object
cannot change during execution that's a
few path that you eliminate from your
mental m of code right
another one is kiss keep it simple
stupid
um the open close principle I'm actually
going to get back to that one on the
next slide
tell don't ask is another one uh also
closely related to encapsulation this
one uh and yeah the principle of list
privilege which is more of a security
concept, but equally you want your
objects, your components to only have
access to what they need to accomplish
their task and nothing else, right? Um
so it's actually quite close in
philosophy. So yeah, all of these
principles more or less advocate for um
sealing off those path that we don't
need, right? Uh but these are just
examples. There are many more many more
principles,
right? So the open close principle um
maybe you've been listening to me so far
and you've been wondering if the close
by default principle um invalidates or
contradicts the open close principle.
The short answer is no, it doesn't. Um
the open close principle is usually
summarized this way. Software entities
should be open for extension but closed
for modification.
And the close by default principle kind
of sounds like the close by default
principle kind of sounds like um that
you should close off everything and not
open up anything at all. Right? Well,
no. If we were to summarize uh if we
were to find an equivalent definition of
uh for the close by default principle,
it would be this one. Software entities
should only be open for extension when
the need arises.
Um, it basically tells you to only open
up your code, to only open up new path
in your code if there is a good reason
for it. Otherwise, it's yagni. You ain't
going to need it.
Now, maybe you are a package maintainer
and what I've just said made you
slightly uncomfortable. Um, for
packages, for libraries, for um, any
code that can be used by people you
don't know, essentially things are a
little different. Package maintainers
need to think carefully about the
extension points they offer in their
code. Right? If you open up your code
too much, then any little change could
become a breaking change. If you close
it off too much, your end users are
probably going to complain about the the
lack of extensibility, right? But if you
mostly work on closed source code, which
I'm sure is the case for the majority of
the people here, me included by the way,
you don't have this problem, right? You
can open up your code. uh you can make
it extensible only if there is a need
for it, right? Instead of trying to
anticipate potential future scenarios.
And that brings me back to that weird
debate around the final keyword. Um
weird in my opinion anyway. If you've
got complete control over your code and
over who's using your code, just mark
your classes as final, right? Um you can
always remove the keyword later if you
need to introduce inheritance for some
reason, but in the meantime, seal off
that path. like you don't need it. It's
a distraction to your brain.
So no, the close by default principle
does not aim at replacing the open close
principle. Um I'm not trying to
revolutionize the solid principles.
Doesn't make for such a nice acronym.
Anyway,
>> all right, time for some concluding
thoughts. Um at the end of the day, no
matter our best efforts, our code bases
will remain mazes, right? But it's up to
us to make those mazes as navigable as
possible. So to sum up, use language
constructs and patterns to reduce the
cognitive surface of your code. Um so
that means um fewer path to explore, um
fewer assumptions to make and also uh
fewer opportunities for misuse.
Be intentional as to where and when you
open up your code. So yeah, it's about
being intentional as to where and when
we open up new path in our code. um
start off close and open up only if
there is a good reason for it.
Use static analysis for consistency and
predictability. Um when all of these
rules are enforced through static
analysis, every developer knows that if
there is a path, there is a reason for
it. There is something at the end of the
path. Right? So this brings consistency,
this brings predictability, but this
also brings alignment.
And this is just the beginning, right?
Once you get into the habit of reducing
the cognitive surface of your code, you
will find many opportunities to do so.
You can do things like uh introduce
value objects for instance. I think
there was an entire talk about value
objects earlier. But if you miss that,
so value objects are uh immutable
objects uh that usually um hold
validation rules as well. Meaning when
you deal with a value object, you know
that the values it holds cannot change
during execution uh and that they are
valid. So that's many path that you
eliminate from your mental m of code.
You can also use immutable date objects.
So datetime immutable is a native PHP
class. You can use name constructors. Um
whatever these are just examples. There
are many many things you can do.
All of the static analysis tools I
talked about today. So uh recctor uh PHP
CSF fixer and PHP stand uh they all
support custom rules. So if there is
something you need that they don't
support out of the box, you can write
your own rule for it. You can also use
other types of static analysis tools
which are more focused on the
architectural side of things. For
instance, uh so there's depth track
which I use sometimes which I really
like but I know that pest um also has a
whole bunch of um architectural rules
that you can use and apply to your code
and you can also use PHP stand for that.
So that's something I learned this
morning with Andre. So I updated my
slide earlier. Um so you can write
custom rules for PHP stand um for uh
architectural decisions essentially.
And finally all of these rules when uh
and especially when they are enforced
through static analysis can serve as
guardrails for AI generated code. So
they provide a a framework a blueprint
that the AI has to follow.
All right. And to finish let me bring
back that definition one last time. So
the close by default principle removes
dead ends for your mental maze of code
revealing a straighter path to
understanding.
Okay. So this is the end of the
presentation. Very briefly on the left
hand side you will find a QR code
pointing to a GitHub repository uh where
you will which is a companion repository
for this presentation. Um you will find
the slides there all sorts of code
samples of everything I talked about
tonight. Um and also configuration files
for the tools I mentioned. Oh, and
there's also a GitHub workflow which is
basically an example use of all of these
tools and rules in continuous
integration. And on the right hand side,
another QR code. This one points to my
company's website where you will find
all of my socials if you want to stay in
touch. And that's it. Thank you for your
attention.