 Welcome to my talk. So this talk is about making upgrading easier for your module users. So who am I? My name's Kim Pepper. I'm the technical director and co-founder at Previous Next. For those of you who don't know me, I've been working with Drupal for more than 15 years. That's the 15-year party tonight for Previous Next. And I'm the subsystem maintainer of the core file subsystem. And I've been an active contributor since the early Drupal 8 days, and I'm a maintainer of quite a few contrib modules. So what I'll cover today is why it's important to make upgrades easier. I'll be focusing on deprecations in this session and how deprecations work. I'll be looking at some of the tools that you can use to check for usage of deprecations in your code, but also how to write your own deprecations in your own contrib modules, as well as some common patterns that you can use and then some just general recommendations. So who is this talk for? So this talk really, my first focus was trying to encourage people who write contrib modules or contribute to core in some way. So developers, essentially, to basically make your modules easier for people to upgrade when they're doing a big site upgrade, they're upgrading a major version, like make their life easier. And I think we heard a lot about making, being kind to your users in Drees keynote this morning. I think this follows the same line, and this is one of my motivations for doing this talk. But also, if you're not a developer, but you might just be curious on how deprecations work, this will kind of give you an insight into how this whole process works. So first of all, why is it important? Well, I talked about that briefly. But basically we have a lot of more frequent releases happening now. So there's a lot more pressure to be upgrading. There's a lot more changes coming. Drupal, Symphony, PHP all have more frequent release cycles now than previous years. And I mean, this is the standard Drupal usage. It's not a super accurate graph, but it does kind of show you what it was like prior to Drupal 8. And then after Drupal 8, we've got a lot more releases. A lot of those are minor releases, but you get the gist. And I think the real point to illustrate here is that there is a cost in upgrading. We can lose users when they find upgrading too difficult, especially major versions. And the cost isn't monetary necessarily. It's just effort cost in doing the work to upgrade. And also, we've got a new release schedule. So this only came out about six months ago. We're trying to have a much more regular cadence for Drupal releases now. So in addition to that, we're going to have long-term support type releases, which are the green bits in here, which is essentially trying to make it easier for people to stick with a long-term release of Drupal and not be forced to upgrade every time a new major comes out or a new minor version comes out. The other side of it is that we actually depend heavily on Symphony. So we're actually forced into some of these releases due to Symphony's release calendar. So we're trying to keep in a cadence where we're sticking with Symphony's long-term releases as well, just to make that process easier. So there's a lot of pressure, I think, on this more rapid releases and more frequent releases and trying to keep on top of things. PHP as well. You might be surprised to know that 8.0 is already out of support. It wasn't that long ago. A lot of people are on 7.4 running their Drupal sites. And so this is happening quite regularly. So as you'll see in this table, 10.0 of Drupal's no longer supported. 10.1's actually running a version of Symphony. That is, well, the minimum requirement is Symphony 6.3, which is no longer supported. And the PHP version is security only. So again, we've got pressure on keeping up to date. But we want to be able to keep up, but we don't necessarily want to break things for our users at the same time. So this is what this talk's all about. So we basically want the stability of the software, but at the same time be able to continuously improve and evolve the code base and move it forward. So basically, this is what I'm talking about is trying to stick to a backwards compatibility promise, which is basically you're not making any API changes between major versions that could break somebody's code who's using that code. And then, of course, we use semantic versioning to demonstrate what that means. So in this example, you might have an example version, 10.2.4, the major version, between major versions, we're able to make API breaking changes. But in those minor and patch ones, we're trying to keep backwards compatibility. OK, so that's just the kind of introduction about why it's important, what sort of challenges we're facing. So I'll talk a little bit more about deprecations now. So first of all, what is a deprecation? So it's essentially a warning that an API is either going to be removed, added to, changed in the future version. And by API, what I mean is any of your PHP classes code that's marked as public, that another user could actually use or call or extend in their code. But it can also refer to other things like config schema, twig templates, reusable JavaScript libraries. That's essentially your area of exposure that other people could actually use your code. So now we'll look at a little bit of how we can tell if we're using deprecated code in our module code or not. So there's a few different tools. The first one I'm going to talk about is the upgrade status module. And this is really a great tool. Basically, what it will do is just look at the current status of your whole site and tell you what modules have an upgrade path or are compatible with the newer version. It will do things like it will check for deprecated code usage, but it's also doing a lot more. So it's doing things like composer version compatibility, core version compatibility, and so on. But underneath, it's actually using tools like PHP Stan. So PHP Stan is a static analysis tool. And what it means is it will check your code without actually executing it. It can check for lots of common bugs, things like unknown methods, dead code, missing return types, things like PHP type hints. It can check for a lot of things, basically because it runs on a system of rules. And we do have a, fortunately, there's someone who's contributed a whole lot of rules for checking for Drupal deprecations. So this is a very basic run-through of how to use these tools. So first of all, it's fairly easy to install with composer via the command line. I hope you're all comfortable with the command line. And really, all we're doing really is installing the MGlamon PHP Stan Drupal module. But you need to add an extension installer first if you haven't got one. So that basically means those rules get installed in the right place. And then we need to configure PHP Stan. So this is just an example configuration, but this is what it looks like. It's a neon file that sits in the root of your project. We can specify things like the path to our custom code. And we can also do things like ignore common errors. So for example, if you're always butting your head against the slash Drupal pulling a service into a class, I mean, there's plenty of cases within Drupal Core that you can't actually do dependency injection. So this is something that you could legitimately ignore. If you add it to your ignore list here, then you won't get warned about it all the time. And then just to run PHP Stan, you just call the analyze command. So PHP Stan slash analyze, space analyze. And then if we've got a deprecation, you should get this nice detailed message. So you can see here, it gives us the actual path to the file that has the deprecation in it. It gives you the line number. And then it's printing out the deprecation notice for that deprecation. And you can see it's very helpful. It's telling you what the function is that you're calling. It's telling you that it's going to be removed in a future version and what else to use instead. And the other thing is that you, instead of... So you can basically, you can set a level from zero to nine in PHP Stan in terms of the level of strictness. So if you want to make it more strict, you can do that. You can start at zero and gradually work your way up. Or you can do what's called a baseline where you can say, okay, well, we want to get to level six or generate a baseline that will capture all of the errors that occur at level six. You then essentially ignore those errors and then slowly pick away at it until you are at level six. So there's two ways to do it. The second tool is Symphony PHP Unit Bridge. Obviously used by Symphony itself, but it's something that we use in Drupal Core. And essentially it's just enabled by setting up a PHP unit listener in the PHP unit XML file. And it will check your code while executing your tests because you all write tests, right? Yeah. Yeah, and tests will fail if you've got deprecated code. And again, it's very simple to install. You can install it directly, but if you in your project run, like install all your developer tools with Core Dev, so using all what Core will use on your project, then it'll require this project. So you'll already get it. Okay, so as I said, it's fairly easy to set up. We just add a listener in our PHP unit XML file. And then you can configure it using this environment variable, the Symphony deprecations helper. The value there is kind of like, there's a lot of documentation about all the things that you can change, essentially, whether you are triggered on deprecations that are incurring in like third-party libraries and things like that, so you can tweak it so it makes sense for you. There's detailed documentation on it that explains all of that. And again, you just run PHP unit to trigger it. And you can see here, we've got a nice deprecation message being output, it tells us what again, what the structure is, what the version is, what to use instead, and then a link to a change record, which is very important. And then in certain situations, you might want to automate this. So if you've got something like, there was a function called Drupal set message, which was used everywhere in Core, that was obviously very difficult to try and kind of manually go through and maintain to get that committed to make all those changes. So a tool like Rector can automate that process for you. So essentially what it does, it just provides some rules to say, okay, this is what it was before, this is what it should be after, and it will go through your code and automatically basically do an intelligent type of find and replace. And there's rules for Drupal, but also for symphony, PHP unit, and Twig as well to automate some of those changes. So this is how you install it. So installing by a composer again, and then you configure it by just running the command. If you don't have a config file, it would generate one for you, and then you just run it. And then to run it, you just call Rector process, and then you pass it the path to your module or your custom code or the code that you want to modify. Okay, so that's the overview of all the tools, how you can use it to check if your code's actually calling deprecated code, but really what we want to do is learn how to write our own deprecations. So if you've got a Contrib module that you maintain, you want to be able to make, like I said, keep things stable but make changes and innovate and improve your code. You can do that by using deprecations. So this is a simple example of a procedural function that's being deprecated. So at the top, you've got an annotation in our PHP doc block saying it's deprecated. Then in the deprecation message, which follows a standard format, needs to have all of these things in the right order. We've got a C annotation, which again is a link to our change record. And that really, the doc block stuff will actually help in PHP Storm or something like that. If you're using an IDE, it will do things like strike out that name or give you an indication that that's a deprecated function. But the real action happens when we actually trigger an error. So when this code gets called, it will call this trigger error. It starts with the at symbol, which will actually silence the error so you don't get a big error being displayed on your site. We've got the name of the function, what version it's deprecated in, again, what version it will be removed from, and then a suggestion to what will be used instead, and then a link to the change record. So the last thing is just the error level for trigger error, which tells it's a user-defined deprecation for PHP. Yeah, so this is the format for the message. So it's deprecated in deprecated version and it's removed from removed version, extra info, C, change record. So it's all stuff that we've seen before, but this is the format that it needs to be in, and all of these things are pretty standard stuff, so from these examples. Okay, so Drupal core uses a change record for every deprecation, but any contrived module can use change records as well. I think they're an underutilized thing in Contrib. I think we should try and use them more. It's got a huge benefit for people to be able to go and find what the clear information about what has changed and why and so on. So while you might get a trigger error, might say use this other thing instead, a change record could have much more detail on a link to the original issue, like detailed instructions on what it was before, what it is after, and you can do things like search for the function name that you might be using or you can filter by versions, release versions. It'll be essentially the first thing someone, when they get a deprecation, notice the first thing they'll go to do, they'll click on the change record link to say what the actual change is for. Of course, we can test our deprecations because we all write tests and this is an example of how you can actually test the deprecation. So there's two things you want to really be doing is making sure the deprecation gets triggered if you call that function correctly with making sure it has the correct message. But the second thing is just to ensure that you haven't broken your legacy function because people are still using it. See, that has to continue to work and the deprecation test has really just said to say, yep, triggering a deprecation, legacy function still works. At the top, you can see we've got an annotation for group, which is legacy. That's actually gonna be required by PHP Unit Bridge and it will provide that expect deprecation function. If you don't have that group, it actually won't work. Yeah, so. And if you want to create, you can create your own custom RectaRule. There's an easy way for you to do that. So Recta, the command line tool has a custom rule command. You run that, you give it the name of the rule, whatever you want to call it and it will spit out like a scaffold for you to generate a RectaRule. So if you know that there's a bunch of things that people have to change, providing a RectaRule will certainly make their life easier to be able to go and automatically change all the deprecations in that code. So I'll talk briefly just about what you can deprecate. There's quite a lot. I won't go into all of this because there's loads and loads of things that Core does deprecations for, besides kind of like the PHP code. So things like services, hooks, plugins, asset libraries, JavaScript, settings, config schema, theme templates, all of that can be deprecated. But what should you be deprecating? So I think the first thing is that you don't know how your module users are actually using your code, how they're extending it, whether they're owning their own custom module that really just extends one of your classes and adds some additional functionality. So you need to assume that any classes or services that are declared public could be being used. So yeah, things like interfaces and abstract-based classes are obvious because they're public, they're meant to be extended or implemented. Yeah, and even config schemas you should be considering to be a public API. So a recommendation is just to try and minimize your public APIs. So this is something that I don't think is happening much yet in Core, but it's happening more and more where we're making classes final, making internal methods private, even marking with annotations to mark things internal, and really trying to minimize interfaces for things that actually need to be interfaces. And I think people think that, I guess the idea is as much like, don't assume that anyone can do anything with your code, you really want to minimize the public APIs just because you're adding to your own burden of maintaining that code in the long term. All right, so I'll talk about a few common patterns for deprecations. So again, I'm not gonna go through the full list of all the different types, this is a few code examples for what you can do. So we talked about procedural functions, this is exactly the same. So the deprecated annotation, the C annotation and the trigger error there. But this also works on class methods, so it's not just procedural functions. So if you've got a method on a class, it's exactly the same. But you might find that you need to add a new service to, say if you've got a class and in the constructor, you're injecting a couple of services and you want to add a new one. You can't just add it in without, you can't assume that the, well, anyone who's extending your class, that will break immediately if you just do that because they don't have that service, that service is not gonna be there. So what you need to do is just follow this pattern where essentially you make it an optional field, not an optional parameter, and then you just check if it exists. If it doesn't exist, then you trigger a deprecation message and then look up the service and set it as a property on that class. So that's usually fairly straightforward for a constructor, but there's also a more complicated constructor example. So if we've got three services that are getting injected in here and we want to remove the middle one, it's not so straightforward. So this is a pattern that I've seen that you can do that. So the first thing is at the top, we're using a trait that's part of Core called the deprecated service property trait, and all that does is you provide the property that existed on that class and the service that it should look up. Then if a subclass tries to access that property, it will trigger a deprecation for it and say no, it doesn't exist anymore, but here it is anyway. So it'll go and look it up and return it. So it won't break existing code just by removing a property. And then the second part is if you look at the constructor, you can see that we've got a union type there for the two services. So what we've done is basically just said, okay, we're removing the bar manager. I should have come up better names than this, but bar manager from this service, swap it out. But if we get a bar interface passed in that second argument, then it means that it's calling the old interface. So trigger, grab the third argument, which is the old one and use that as Baz, and then trigger a deprecation error. So this gets around that problem of being able to remove things. So interfaces in abstract classes is also complicated. So you might think, oh, well, I can just add a new parameter to an interface method. Well, as soon as you do, it will break any code that is implementing that interface because it doesn't do it yet. So you don't want it to happen. So it's a kind of a more difficult situation. What we do in core is we basically just add that new parameter as a commented out parameter. You tell it the type. And then we're essentially just saying this is gonna be coming in the next major version. We warn people that it's coming. The symphony debug class loader will actually trigger a warning about that and say, next major version, this will be required. And then we can't actually add this in until the next major version. So just at a high level. So if we wanna deprecate a whole interface, the whole class, we do that at the class level. So the trigger error can go in just under the namespace here. And then the annotations would just go on at the dock block at the top of the class. So it's just moving where things are. For concrete classes, so by this I mean things like you might have value object or something that you're creating. You can put the deprecation notice at the class level dock block but trigger the error in the constructor. So every time one gets created, it'll trigger an error. And following the same pattern. So services, it's pretty straightforward with services. You just have a, we have a support for adding deprecated to the service definition in your service.yaml file. So anytime someone tries to use that, we'll get a deprecation warning. And then hooks, hooks are slightly different too because normally the convention is that you have a .api.php file that documents the hook. So you can put deprecated in the api.php file in a dock block. But then when you invoke that hook, you need to be calling invoke deprecated instead of invoke or invoke all deprecated instead of evoke all. So that will trigger the deprecation if that hooks gets called. And then plugins is the same really as a class. So most of the time plugins are classes. But there are other types of plugins, ones like yaml files. There isn't any support for that at the moment, but there is an open issue to add support for all plugins to be able to just, you know, deprecate things at the plugin manager level. Okay, so that's a few examples of how to do deprecations. So now, you know, just a few general recommendations. So I think how you release your Contrib module is important in terms of support. So you should try and support your, the next major version of Drupal in the last minor version of your Contrib module. So for example, in this example, you know, we've got a 1.x branch. We wanna introduce Drupal 11 support in 1.2 and support two versions at the same time. And then when we get to our 2.0 version of our Contrib module, we can drop the Drupal 10 support. What this does is gives people an opportunity to upgrade to the last version of your last minor and be able to get support for the next major version of Drupal without having to do a big jump. So we don't want to kind of abandon them and go, now you need to go to version two and work it out for yourself. And there is some support in Drupal for supporting two versions. So there's this deprecation helper, backwards compatible call, which basically we just checked the current version of Drupal. And then, you know, if it's before that version call one, if it's after call the other and trigger deprecation too. So that will help with moving between supporting multiple versions. Okay, some other basic recommendations. So I think it's worthwhile. I mentioned testing a few times. I hope people are doing testing. Just having good test coverage helps you catch regressions if you are making big changes to do your code functionality. So obviously that's very important. You can also, when you do do big releases, I think it's good to put as much detail as you can into your release notes. Matt Glumman's got a great little CLI tool that will generate nicely formatted. Release notes between versions. And then, yeah, be polite and responsive in the issue queue. I don't, I mean, it seems like an obvious thing, but you know, these users are often coming to you and they've got an issue and they might sound hostile, like this thing is broken, whatever. Just remember that, you know, you're trying to encourage them to use your module and actually encourage them to kind of get involved. Okay, so quick recap. So I talked about why it's important to make upgrading easier. We looked at how deprecations work, what they are, how they give users warnings of upcoming changes. I looked at some of the tools like PHP Stan and Symphony PHP Unit Bridge to trick, to basically warn you of those things. And then I took you through some common examples of patterns for adding deprecations to your code to making sure that things run smoothly. So, yeah, I hope you found this useful and you're inspired to go out there and add some deprecations to your contrib module code. And I think you've got, I've got any time for questions. Right, yeah. Thanks, I'll take that as a comment. Yeah, I mean, I think the message I'm trying to convey is like, yeah, don't be sort of, they try that, yeah, I think we do that in core, right? So the last minor version, we don't try to add anything new. We basically try to make that just an upgrade version. Yeah, thanks, Jess. I think that's got to do, I mean, every contrib module is gonna be different. And I'm trying to kind of take some of the core concepts and apply them to contrib, right? So it doesn't always match up, but, you know, often if you've got to say a contrib module that is, you know, using an underlying third party library, for example, and that's got a major version bump, you might be trying to, you know, line your major versions up with that, that library's major version. So, you know, it's not always gonna be straightforward, but I think, you know, obviously we know when Drupal releases are coming out, so aligning your modules with those. And I think, you know, going back to that upgrade status module, like the objective is to just make that green, right? So that somebody who's using your module comes along and they go, yep, okay, I don't need to worry. I don't need to go to the issue queue and find out what's going on. You know, you get your version compatible with the next major and make that process smooth for them. Yeah, cool. And I just wanted to invite everyone to our party. Yeah, you got a question? I know, I know, wrong talk. Oh yeah, I do, I will post the link. So I'm gonna post these slides and I'll put links into the deprecation policy stuff. Like it's really useful. There's a lot there and I couldn't get through it all, but yeah, I'll definitely post the links to the deprecation policy page with these slides. I think, yeah, I did think about that. But I think it means that essentially you're having to support more versions with your contrib module and so, and I'm not sure that that dribble deprecation helper might be useful for like, you're not gonna have three different versions running in that. You might have to write something that would support three different versions. Because essentially that's what would be supportive, right? But yeah, it's a good question.