Submind YouTube summaries
Thumbnail for Escaping the Code Maze - Yannick Chenot

Escaping the Code Maze - Yannick Chenot

Watch on YouTube

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.