 How many of you have attended a conference like this before and you've learned about all these new techniques and tools and you thought, I'm going to go and implement that on the next project I work on? Then you go back to work and you start in that project you've been on for months with old spaghetti code, no one tooling, no test suite and you never get around to actually modernizing the code base with all the cool things that you've learned about? My name is Michael Stjellan, I'm a developer at Previous Next and if you'll excuse the clickbait title, I'm going to give you five simple tips to level up your legacy code. So here's some comment traits that you might find in legacy code. 2000line.module and.theme files, massive functions that consist of deeply nested if statements and for each loops, deprecated function calls, a API, no consistency, no comments and of course no tests. I'm sure you've all come across this sort of thing before, you wish you could just throw it away and start again but your client or your boss would never agree because there's no perceived value in that. This is fairly typical of code bases that have been passed around a lot, maybe you inherited it from, I don't know, maybe it was outsourced to a cheap international agency or it's being neglected by your own team or maybe it's one of your old projects but you have come a long way and you know a lot better now. When you have a message code base like this, it's a nightmare to maintain. You get a request to change something but no one actually knows how the code works. So you just add some more nested if statements in there. Cross your fingers and hope for the best. Let's talk about how we can get this into shape. I'm not going to ask for a show of hands here but I'm sure there's a not insignificant number of people here who don't write automated tests. I know this because I was one of these people for far too long. Perhaps you've heard about tests but you don't see the value in it or maybe you want to but you don't really know how to get started or maybe you think I don't have the time for this. I might add some, you know, when there's all that free time before go live. So this is the most important tip. So if you take away nothing else from this and you want to go and get a beer, just listen to this. This is the one, okay? So why should we write tests? First of all, when you're developing a feature, by writing a test you can confirm that the feature does what you want it to do. Don't be fooled into thinking that your feature is too simple for a test. For example, maybe you've been asked to create a simple article content type with a views page that, you know, shows the most recent articles by publish date. Maybe there's some additional logic to show a feature item at the top. It sounds pretty simple but this is worth writing a test for. Not only can your test confirm the feature most correctly in the most typical scenario, you test that it, sorry, I skipped ahead there. You can also test different edge cases such as for different user roles, for unpublished content, etc. So the other reason to write a test is to ensure that the feature continues to do what it's meant to do. This is important if you plan to make any changes at all to your code base in the future, including when dependencies are updated such as Contrib or Core, when features are introduced, when other developers refactor your code, or when you refactor your own code. But having a suitable level of test coverage that gives you greater confidence to roll out any of these updates without having to manually QA every single part of the site. For example, my colleague Lee has a project that he updated some dependencies on and the test suite started failing. The test that failed reported that dynamic page cache was returning an uncashable response, but he had written the test to make sure that this particular page was cacheable. So by running the automated tests, he found a bug in the admin toolbar module. He was able to report that bug, submit a patch and have it fixed within a day and that prevented that same bug from going out to, I don't know, probably thousands of sites. Well, that module probably, I don't know, hundreds of thousands of sites. And it might not have been picked up until someone was in there administering the site and thinking, gee, it's a bit slow loading here because the cache isn't working. So also by having the tests, it means you can make wholesale changes to your code and get immediate feedback about what other maybe unrelated sections might have broken due to your change. So how do we write tests? There's many different ways that you can write tests and many different types of tests. So it's important to find what's going to be the most appropriate type. The industry standard tool for testing PHP is PHP unit and you can write different types of PHP unit tests. In Core and Contrib, you'll typically find unit and kernel tests, which are great for testing small units of code in isolation. They're very quick. They test the units on their own, but they don't really help to guard against updates to other areas of the site that don't interact with these units. Same goes for functional and functional JavaScript tests, which use a, well, they simulate a browser to interact with your feature, but the way they work in Core and Contrib is that you're building up like a dummy environment with the minimal set of modules and configuration that you need just to test your feature. So as you can see, none of these really help to test your site as a whole. How all your features integrate together. So you really need to be testing with something like Drupal test traits. This provides traits for writing functional tests against an existing site rather than the made-up database that you're using in your previous tests. So typically this involves having a sanitized development database or if your site can be installed from like Drush site install, maybe that. And we run the tests in this environment, which already has all your modules installed, all your content types defined, your views, you know, maybe even some sample content or perhaps even some real world content. So let's go write a test for our simple article feature. So this is an overly simplified example, but basically here we're creating three article nodes. The first two will be published and the third one's going to be unpublished. Then we do a Drupal get. So we're pointing our browser at the articles listing page and then we perform an assertion that we can see the first two published articles and that we can't see the unpublished articles. You might notice there, so I've abstracted that a little bit, so I created functions for create article and assert article exists or assert article not exists. And by creating these helper functions, it makes it a lot easier, reduces the friction to create these tests. The more of these helper functions you create, the easier it gets and you get to a point where you really have no excuse to not write a test. So make sure you always write a test for every new feature or every change to an existing feature and when you're giving estimates, make sure you allow time in your estimates to write these tests. Once you're happy that you have at least some level of test coverage for the most important interactions on your site, then you can move on to the next tip. For this tip, I'm going to share what I think are the most important tools for ensuring code quality. These tools are for everyone, no matter the experience, and they help to make code reviews easier. They catch issues early during development, before testing, before code review, before QA, and before deployment. The first quality measure we can check is code style. It doesn't matter what coding standard you use as long as it's consistent across your whole project. You can start with the Drupal coding standards and you can tweak them as you see fit if you've got specific requirements or there's something you don't like or something you prefer. So the easiest way to adhere to the standards is a tool like PHPCS or PHPCS Fixer and have it integrated in your IDE. This is simple to set up with the Coder module and with PHP Store. And by having the code style analyzed directly in your IDE, you get instant feedback as you write in the code and you can fix it up straight away. You can enforce the coding standards with a Git pre-commit hook to run PHPCS before a commit. And finally, to make sure that everyone on the team has the same standards, you can build it into your CI pipeline, which I'll talk about a bit later. Moving on, static analysis is pretty cool. It catches bugs before your code is ever even executed. So the two most common tools in PHP for this are PHPStand and SAM. Trouble Core uses PHPStand, so I'm going to focus on that. Similarly to PHPCS, you can set up a set of rules to analyze your code. And PHPStand can help you detect deprecations, missing or incorrect type hints or return types, use of potentially undefined variables, dead code paths, and many other problems. So here's an example from a project where I recently added PHPStorm here. And I don't know if anyone can spot the bug here. So what the code is doing is we're checking, we're looking for a config value for our GTM container ID. If it's not found, we're just going to return. We don't care anymore. If it is found, then we're going to do some stuff. It doesn't matter what it is. And then we're going to attach our custom analytics library to the page. So after I ran PHPStand, I got this error message saying the negated Boolean expression is always false. So what that means is that if statement, checking if the container ID is false, that never actually evaluates to true. Because if you can see at the top, we're prefixing the config with the string gtm- so that's always going to return true. It's an easy one to miss. And as you can see, that's been actually in production for over two years. Sorry, I meant to censor out the name there. So, yeah, as you can see, PHPStand is really great for catching those sort of bugs and luckily the impact of that one's minimal because we already have the configuration value set. So there are a number of approaches you can take when you're adding PHPStand to a project. Basically, you can use predefined levels that vary in the amount of strictness of different things that they're going to try and detect, starting at level zero and going up to, I think, level nine. So if you're starting fresh on a new project, I'll probably start fairly high, like level six or above, and try to understand each of the issues that are reported and look for a solution. You can add exceptions and you can disable individual rules, but usually, you know, if you can look for a solution and find out that's going to be better, there are some drupalisms that can be awkward to work around, but as I said, yeah, there's usually a solution. So if you're adding PHPStand to an existing project, you could, like one approach is to start at level zero and try and fix everything in level zero and then move on to level one and continue until you get it as strict as you like, or you can also create what's called a baseline. So the baseline basically acknowledges all the existing problems in your code and allows you, you know, to continue with that, but any change to any other piece of code is going to be at that strict level that you set it to and you basically chip away at the baseline until you've fixed all the issues, but every other piece of code is analyzed in a strict manner. So that might be the way to go if you're ready to embark on the next tip. So now that you have your safety boots on, it's time to start cleaning up your code. I don't have time for a deep dive into all of these topics, but there's plenty of literature online and other talks at this conference and other conferences. So let's talk about bundle classes. My colleague Daniel spoke about these earlier this morning. If you didn't catch it, make sure you go and check the recording. Let's see if we can get a link up here, you know, in the top of the video. So this is a relatively new feature in Drupal Core that I think is one of the most exciting developer experience improvements that we've seen for a while. The idea is that each content type or vocabulary or media type has its own PHP class that inherits from a base class. So whether that be Node or text on any term or media. And that allows you to define your business logic for your content types in their own classes. For example, an article might have a getOrtherBio method that's going to return like a themed AuthorBio that you can display, you can call it from a block plugin from a twig template or wherever you want to display it. There's no hard and fast rules about how you should use bundle classes, but I find that the more I use them, the more excited I get about them. Anywhere that you find your code base checking for a particular content type or bundle before applying some logic, you can probably move the logic into a bundle class. The same goes if you have common functionality. For example, if you have content moderation workflows, you might have bundle classes that implement a moderation interface and use a trait for methods like getModerationsState or setModerationsState. So in this example, this is a before and after, so, well, this is before. You can see we've got a, this is a custom block plugin that has a method to get the subtitle that's going to display on like a hero banner type thing. And it's got these if statements, which we don't like. So we're going to check if it's an event. Then we're going to look at the event type field and try and get a taxonomy term from that. But if it's an article, then we're going to look at the category field and try and get a term from that. Then if we have a term, we're going to get the label and if we don't have anything yet, then we're going to go to some fallback. So after we move the logic into bundle classes, basically we can check if the entity we're looking at implements our custom interface, which is entity with subtitle interface, or you can get creative with your naming. And if it does, we just call entity.getSubtitle. So then our bundle classes would look something like this. So our event class will have, will implement that entity with subtitle interface and provide the implementation for getSubtitle, where it calls another helper method here, which is getEventType. Article node does the same thing, but it calls getCategory. And then if you had a third content type that you wanted to add support to this, you know, hero, banner, block thing that has subtitles, you could just create a new bundle class and you don't have to add another if statement or switch statement or whatever to that code we were looking at before. So the getSubtitle method could do something completely different. Maybe it's going to create a random dad joke or something like that. A quick shameless plug. Sorry, I got to go back a couple slides. Normally to declare a bundle class, you have to implement entity, bundle, info, auto, something like that and list all of your bundles and all of your classes. I've created a module called bundle class annotations, which lets you put a little annotation at the top of your class and then it'll automatically get detected and you don't have to implement that hook everywhere. All right, moving on. Whenever you implement an alter hook, ask yourself if you could create a plugin instead. So instead of implementing hook preprocess nodes to alter the appearance of a field, perhaps you should define your own format for the field or if you're using hook block build alter, you can define your own block plugin that extends from the original block and that way, basically, you have all of the logic for your plugin directly in that PHP class rather than having the base class and then having random hooks all over the place that interact with these plugins. There's plenty of other areas where you can use plugins. Views has heaps of different types, like displays and filters and exposed forms. If you've ever tried to hook form alter a views exposed form, it's pretty difficult to even figure out which view you're looking at. So if you use a plugin for that, it's a lot cleaner. PHP 8 has introduced some nice language features that help to improve the quality of your code. So constructive property promotion reduces a lot of boilerplate code at the top of your classes, like you declared five different properties and then you've got to document them in your PHP doc for your constructor and then you've got to put them as arguments for your constructor and then you've got to initialize them in your constructor. But with CPP, for short, you do it all in one line, it's a lot neater. Match expression syntax is a bit nicer than switch a lot of the time. There's new string functions that return like a boolean rather than the string position and substring and all of that dance where you get maybe a minus one or a false or a zero or... Yeah, a bit ugly. And there's plenty of other changes in newer versions of PHP so it's important to keep up to date with these. And in some cases, if you can't upgrade straight away, you can use a symphony polyfill to get access to some of these functions while you're still using an older version. And when you're refactoring the general rules for clean coding apply, don't pollute the global namespace. So don't put everything in your .module file if you can use classes and services and dependency injection, that's a lot better. Another kind of shameless plug, Hux, my colleague Dan Finn, made that, or maybe it's hooks, I don't know how to say it. But that lets you declare your hooks in an object-oriented manner and then you can use dependency injection for that as well. Yeah, to reduce nesting and make your functions a lot easier to read, you can fail fast and return early and favor smaller functions that do only one thing. So if you've got a 200-line function, maybe you should break that up into two or three or four or five functions. Just makes it a lot easier to read. All right, our next tip is the glue that holds everything together. So there's no point having automated tests that don't ever get executed, there's no point configuring coding standards that are not enforced, and there's huge benefits to performing the static analysis for every change that's introduced. To ensure consistency and repeatability, you need a CI pipeline. Most of you are probably familiar with this already, but if you're not, basically every time you push a code change, you trigger a predefined set of tasks that run, and if any of them fail, your code will be rejected. There's many different services for this, like Circle, GitLab, GitHub Actions, Bitbucket Pipelines, and they all pretty much do the same thing, just with different syntaxes and slight variations here or there, so use whatever is easiest for you if you're already using GitLab, then use GitLab CI or take your pick, doesn't matter. So what does a pipeline typically do? I'd suggest something like the following. So first, we generate our build artifacts, like composer install, NPM install, and then we can run our code quality tools, PHP, CSS, and PHP stand. Then we're going to prepare a test database. You could do that with Josh site install with your installation profile or by existing config, or you can use a prebuilt and sanitized database, maybe from a database dump file, or we use pre-packaged database container images. Then we run our automated test suite, and only if everything passes up to this point, then it can be deployed to a QA environment so it can be tested before it's merged. So the final tip for today is to perform regular maintenance. I'd recommend weekly dependency updates. Ideally, you want to have an automated job, perhaps in CI, that will run composer update, then perform the database updates, explore the configuration, commit the code, and create a pull request for you to review and merge. Since you have good test coverage, quality tools, and your CI pipeline, you should be able to do a quick review and merge while you're drinking your Thursday morning coffee. I say Thursday because in this time zone, that's when the Drupal security window is, and if you're applying the updates weekly, then when a critical update comes out, you won't be stung with a backlog of all these other updates that you've got to apply at the same time, so there's better chances that it's going to pass straight away. By staying on top of code deprecations, it helps to ease updates to newer versions, make sure you periodically scan for deprecations and switch to the recommended replacements. You can also use the upgrade status module to check compatibility with Drupal 10 or whatever the next major version of Drupal is at the time. This includes making sure your contrib modules are compatible and that your custom code is as well as your environment, like if your PHP version is too low or minus fuel or whatever. And if possible, try to build a regular mandatory maintenance period into your sprints. So perhaps on the first day of your sprint, you'll get together and work on particular, you could have a backlog of technical debt that you want to pay off or developer experience improvements that you'd like to see. And yeah, use that time to spruce up the code base. So what happens next? Code review becomes easier. Developers have confidence to write new code, modify existing features and update dependencies. The code base is more enjoyable to work with and bugs are caught before they make it into QA or into production. If anyone has any questions, I think we've got, well, we probably don't have a few minutes left, but you know, I got 26 minutes and 16 seconds, so all right, let's go. Yep. Do we want to do a... Oh, I can be loud. It's all right. I don't know if that works, but... Too quiet. Yeah. All your code is now packed. And secondly, if it's not, we're doing the Code Sprint on Royal Pride. Do you mean my client's code? Yeah. Well, I guess I've got a lot of different projects that I work on, and we do the Thursday patch jobs every single week for all of them. So, yes, they're all up to date, but if you're asking about, you know, do they all pass PHP Stand Level 9, then the answer is no. And on Friday, I'm going to be focusing on Drupal Core or Contrave Modules, so no. Yep. Any suggestions on how to retrofit the testing into code that doesn't happen? Yeah. So, I guess the types of tests that I was talking about with the unit tests and kernel tests, they're going to be really hard because to write good unit tests, you kind of need to write the code as if it's going to be tested with a unit test. So, those Drupal test traits, I think that it's more like how would a user interact with this in the browser and that it's a lot lower friction because it's like, you know, go to this URL, click on this button, and see that this thing appears on the page. So, I guess identify, like, start with what's the most important interaction on your site and test that that works correctly. And yeah, go from there. Okay. Well, otherwise, yeah, one more. Last one. Yeah. Have you tried B-Hat or the testing rather than the unit tests? Yeah. I actually haven't used B-Hat testing. I know that some people that have defined the syntax of it, well, it's meant to be more human-readable and kind of translates to business requirements. But you still have to kind of have a translation layer of, like, you're writing this stuff in this human language, but then you've got to translate it to PHP. And I'm guaranteed almost all the time that people, like, the stakeholders aren't looking at your tests. So, I think they're only people that it really matters to are the people that are writing the PHP anyway. So, I'll do it all in one language. But essentially, it uses the same, like, mink browser thing in the back end anyway. So, it's kind of doing the same thing. It's just using a different syntax. And you've got, yeah, access to all of your PHP classes and everything there. Is that answer that question? Does it support browser? Yeah, yeah, so you can, so there's the, you can have a, like, a functional test or a functional JavaScript test. And the difference being that, like, the functional test is just, like, a simulated mink browser, whereas the JavaScript one is, you can use, like, Selenium Chrome or Headless Chrome or whatever. And you can even set up, like, VNC to, like, have, like, a, you see the browser, like, running as you're doing your tests. If you have, that's only, like, if you're using those JavaScript tests. Otherwise, if you're just using the, like, the functional test, they spit out HTML output of every, like, get request. So if the test fails, you can go back and look, okay, what actually happened during the test here. All right, I think that's everything. So have a great evening.