Developing Symfony applications in 2023 [eng]
In this talk, we will delve into the current state of Symfony development in 2023, exploring the latest features, tools, and best practices that have come to define the landscape of this robust PHP framework. We will take a deep dive into the innovative strategies for building scalable, secure, and highly performant applications, with a focus on leveraging Symfony's latest updates and enhancements. Whether you are a seasoned Symfony developer or a beginner looking to broaden your skills, this talk will equip you with practical knowledge and insights to build cutting-edge Symfony applications in today's dynamic digital environment.
- Nicolas works nowadays on Symfony, with two sides to his contributions
- As a member of the core team, he helps by helping others to contribute, by reviewing pull requests, by fixing bugs or by modernizing the code base. He puts a lot of effort into making Symfony fast, smooth and extensible, without compromising
- On the business side, he leads the ambition to create a sustainable company supporting and supported directly by the Symfony ecosystem
- Twitter, GitHub
Talk transcription
Thank you. Thank you for the introduction. Hello, everyone. I'm really happy to be here today with you remotely. I hope we'll be able to meet in real life sometime soon. So, I'm going to tell you a few things we did in the last month related to Symfony, of course, where the community contributed. Of course, you know that Symfony is a fantastic set of low-level building blocks. That's Fabian, a slide from a previous talk from Fabian. What is nice is that you can then assemble to develop anything you need with Symfony. So, there are two sides to Symfony. There are these building blocks, we call them the components, and there is, of course, the full-stack framework experience where you start with Symfony and keep Symfony at the heart of your application.
So, there's not only the component in Symfony. There is also the Symfony local web server, which has many features to improve developing with Symfony. Like providing an HTTPS local web server, HTTP2, it manages your command line, etc. So it's really the companion of developing with Symfony. There is also the Maker Bundle, which is this bundle that generates the code you need so that you can be very efficient with Symfony. I'm mentioning those because they are really part of the modern Symfony experience, but they are not in Symfony Core. And they still make Symfony work. So it's really, really nice to develop with.
In Symfony itself, in recent years, TID told us that we could invent something we call now auto-discovery, auto-configuration, auto-wiring. And I'm sure you know that because it's not new to 2023, but it's still really core to the Symfony developer experience because it means everything works out of the box. You start coding and keep your editor in Symfony. In the PHP page, you don't have to switch to configuration and back and forth. That's really great. The newest thing that we are still adopting in Symfony and in the PHP community is, of course, attributes. Attributes change a lot of things in the Symfony ecosystem. And you'll see many examples just after where you can use that to declaratively help Symfony, the core, better. So you can wire or use your code.
This is an example. When you create a controller, you can now use the route attribute. You can also use is granted. Maybe you were used to those attributes or annotations before. What is new to this year is that those are core native attributes now. This means that you don't need the framework extra bundle, which was an outside dependency. It was not perfectly integrated. And now this. This is core. And this is because PHP eight makes this core. We can leverage that.
Another example is this one. This is not on Symfony itself, but it's on the doctrine side. There you can see some doctrine mapping because doctrine also is using attributes now. And there you see two things. Actually, the move from annotations to attributes. And you can also see that now with the integration, with the proper typed properties, you don't need to declare the type of the column if they fit the type of the property. So when we declare a string email, then the mapping will automatically know that this needs to be a string column. And that's nice because it makes your code just cleaner and easier to use.
Another example. Maybe you're already using that. It's pretty recent. Also, when leveraging auto-wiring, sometimes the type of the argument is not enough for Symfony to figure out how it should wire things. So what you can do now is use the auto-wire attribute on arguments to basically tell what you need there if auto-wiring is used. This makes it very easy to use that without any other kind of configuration wired into your application. And this makes it also super easy to test because the attributes, as you know, they are just declarative pieces of information. And if you use this code in another context where the attribute means nothing, then it's just ignored and in tests, that's what you want.
So this is a service, the plot factory, but you can do more things with services. So this is the same plot factory, but now it has this auto-configure lazy true attribute. And by using that, you can then tell Symfony that this class is a heavy one in terms of dependencies. So you are telling Symfony to not actually initialize this class when it's not used. I mean, a method must be used before initializing the class. So this is really nice when, for example, a class requires a database connection, and this could happen in the constructor. Then you can postpone the heavy call to the network to the moment where you actually need that. And this is really useful for performance and also sometimes for just wiring because laziness allows creating kind of circular references, and that solves issues in real big applications.
The auto-configure attribute is not only capable of enabling this lazy true flag. It can also, for example, add a tag on the service or add a method call when a class is declared. And thanks to this feature, we created subclasses of the auto-configure attribute that are specialized, and they embed their tag, basically. So those are some examples. They usually start with "as" because it's telling Symfony how you want those classes to be used. So as an entity listener or as an event listener or as a command, as a controller, as a message handler. So there is a collection that is growing from version to version to declare how Symfony should use and wire your classes. That makes the code very declarative. Everything works just thanks to these metadata. And I really like it. The more I use it, the more I like it. I hope that's going to be the case for you, too.
Back to Symfony. So this is today. Today, we are working on version 6.4 and 7.0 because we are closing two years of Symfony 6. So we just released Symfony 6.3, sorry. Symfony 6.2 is not maintained anymore. And we still have one long-term support version, which is 5.4. Symfony 6 is, as you know, maybe PHP 8.1 minimum. Every six months, we do a feature release. This is the classical date-based, period-based release cycle of Symfony. For every minor version, it's very easy to upgrade because we have the backward compatibility premise, which is basically semantic versioning applied to Symfony project. And this means that upgrading to the next minor doesn't break anything. So you can just adopt the new features. And that's what you should do.
I mentioned Symfony 7. So Symfony 7, like Symfony 6 and 5 and 4, we know how to do it now. It's not going to be a big bang release. So we bump PHP to the minimum version 8.2. That's something that happens every two years. And we have a continuous upgrade path. So the continuous upgrade path is a way to make it very easy to move a project from Symfony 6 to Symfony 7 despite the backward compatibility changes that we are going to have in Symfony 7. Basically, it means that we add deprecations in Symfony 6 before removing them in Symfony 7. And this is a must. We don't do changes in Symfony 7 if they cannot be advertised before as a deprecation in Symfony 6. So basically, what you can do then is every six months also try to clean the deprecations so that you can then be very ready easily for the next major. And do that every six months. And I promise you it will make maintaining Symfony applications in the long run very cheap.
So, in Symfony, in the recent month, we have, like, I don't remember exactly, but many pull requests, many hundreds of pull requests. And many of them are labeled as minor. So you don't see them in changelogs. That's still almost 40 percent of the activity of the repository. So that's a lot of activity. And I selected the three things that you might want to do also on your application to modernize the code, which is what Symfony did. There are many more examples. But this one is related to the type system. You know that PHP is improving its type system. So Symfony is also adopting more types.
For example, we added return types to interfaces that didn't use them. We added some missing template annotations for generics and added more generics to the code base. So there are a lot of pull requests that come in the form of generics like this one. For example, it's a service locator in Symfony that implements the service provider interface. And there you see we have those PSALM or PHP standard annotations that tell the static analyzers that, OK, this is a service locator, but it's going to return specific kinds of objects in its methods behind that. So it makes it more easy to track bugs and know how the code behaves just by reading the code and the classes. And we are also getting ready for PHP Unit 10. So the test suite is still running 9.6, and PHP Unit 10 is a lot of work for us because internally it has many important changes.
Still, there are things that we did in the test suite, like removing withConsecutive, which is deprecated, migrating to static data providers because this is going to be required in PHP Unit 10. For example, another one is using the test case suffix for abstract test cases. So if you have a test case that is abstract, it must be named something test case. That was the case for most of the classes in Symfony, but not all of them. And that's also something that you are going to have to do if you want to modernize some existing codebase.
And of course, thanks to bumping the minimum version of PHP in the Symfony codebase, we can leverage more new features. For example, the arrow function syntax for short closures. So this is going to be exactly the same in terms of behavior. But the code is easier to read. Basically, it's shorter, easier to grasp. It removes ifs and else and etc. So use colon class instead of the get_class function and similar constants, which is also a nice cleanup. And there is this null coalescing assignment operator, which is also nice to save and save isset calls and make things very much shorter. So I really like those changes. And it's great that we can bump the minimum version of PHP.
Of course, you might be more interested in features, so I selected a few of them. It's going to be kind of a list. Sorry about that. I don't know how to make a consistent story out of the features. And the reason is that, as you know, as I mentioned before, we release Symfony at a fixed date, and at this date, we release what is ready and what is ready is never what we planned for because we don't plan for anything, we just review per request. And when they are ready, when the core team and the community is thinking that this is a nice feature to have, then we merge that and we ship what is ready. So it's kind of random. Of course, there are still stories to tell, like attributes, for example, is a long-running effort. And yeah, we are still adding more attributes to the code base because we just didn't have the idea to use this attribute in this specific context, and now we say, oh, but it would be great to do that. So that's how Symfony moves forward. And I'm looking forward also to the next pull request that the community is going to send. Maybe that's going to be.
So we released a few new components. I didn't list them all, but those are the most important. First, we released almost one year ago the clock component with which makes it easy to mark the clock, basically. So instead of doing new DateTimeImmutable in your code base, you would use just a now function that will do the same. But internally it will return it will use a mockable clock so that in your test you can rely on something else than the real clock.
We also published a scheduler component and the scheduler component is basically a way, an improved way. Something a way to replace crowns and crown tabs. You don't need crowns anymore if you installed the scheduler component. And the really nice thing about the scheduler component is that it is leveraging the messenger component to trigger the ticks of the schedule. And this means that you just need to have messenger in your infrastructure to have it work. And this is nice because most applications do already have messenger in the application. Which means if you remove crown, that's one less thing for DevOps to care about. So simplifying the stack by making the scheduler components rely on messenger.
Another component which is really, really promising to me, I really love this way of improving the web stack, it's the asset mapper component. And this one allows us to remove Node.js from the developer experience to build nice, fancy JavaScript-enabled user interfaces. And the difficult point with those modern interfaces is, of course, bundling JavaScript libraries together. So using Webpack or something like that to pack many JavaScript files, resolve dependencies and etc., etc.
And it turns out that modern browsers, they have something called import map. Import map allows removing all that by moving all this to the browser so that the browser can resolve the dependencies, download the compressed asset from a CDN where they are waiting pre-compressed, pre-compiled, and leveraging HTTP2 so that this is very fast. Basically, it means we don't need Node.js. We don't need a bundler anymore for many use cases. Of course, there will always be single-page apps, for example, that will require a bundler. But for many JavaScript applications that just need an enhanced user interface, the asset mapper is going to do the work for you.
The last components I'm mentioning here are a couple of components: remote event and webhook. These help accept payloads from remote APIs. For example, when you send emails, you might get bounces back. When an email fails to be sent to the recipient, then providers can send you a webhook, a JSON webhook, where they call you, telling you this email didn't reach its recipient. And leveraging, providing this callback endpoint and then managing this JSON is what remote event and webhook are about. Basically, there is a way to model the incoming payload through a data object, a data transfer object, and then to put that on the messenger queue so that then some offline process can do something with the message, update your database records or I don't know. There are already some built-in providers that know about the schemas of Mailgun, for example. And we are waiting for more providers. I think a few pre-requests came already on the topic. And we're waiting for more.
On the controller side, we added new attributes: map query parameter and map request body and query string. Let me show you an example. So this is a controller. And there you see we have a parameter named page. And we'd like to have this on the query string. And in order to read that, you have to use the request object and write request query and then get page. And instead, in 2023, you can just do that. You add a map query parameter and Symfony will automatically do the mapping for you. It will take the value from the query string and inject it. And it can also do some validation thanks to the filter native PHP function that we are leveraging internally.
Now, this is about turning an incoming payload like you receive in a JSON endpoint. You receive a JSON. And you want to map it to some DTO. So this is the DTO that we want to hydrate from the JSON that comes in. So this DTO has two properties, a comment and a rating. And then you can see there are some assertions on those to verify that the content is valid at some point. And thanks to recent improvements and new attributes, you can map this very easily from your controllers. So there you see we have a route. And we add an argument, which is a product review DTO, the class I just showed you before, and thanks to the attribute, which is map request payload, internally Symfony will turn the JSON into a DTO using the serializer component and the validation component to both hydrate and create the object and then validate it. And if the payload is invalid, Symfony will generate a 422 or a 404, depending on the situation, etc. So basically, it makes it super easy to accept incoming payloads and map them to your business logic then after that.
More components, HTTP clients; there we added a feature to make it very easy to use URI templates, which is something that is described in RFC and that is super useful for hypermedia APIs. We also allowed you to declare multiple base URIs so that when you want to hit a relative URL, it's going to be related to maybe three different servers. And this is something that you can use in high-availability setups where, for example, you have three Elasticsearch servers and maybe you want your client to go to the first one, but when it's done, you want it to automatically go to the second one. And this is what multiple base URI has for. And the last item on this list is about raising the minimum TLS version, which is something you may not care about. What is good to know here is that Symfony is bumping the bar and making it more difficult to hack your system because the previous versions of TLS are broken, basically. So if you use Guzzle, for example, that's something you have to do explicitly and only in the recent version. So Symfony is going to decide for you what is the best minimum TLS version. Of course, you can configure that if you want to do it differently, if you want. So more items.
The security system: I noted two attributes to do validation. So no suspicious character is going to verify that, for example, a username doesn't contain suspicious characters. What is a suspicious character? It's, for example, when the username contains a mix of Latin and Cyrillic letters to spoof and to do basically spoofing. Yes, spoofing items, identity thefts. New password strength constraint. This one is about verifying that the strength of a password is strong enough. The strength means length and also the characters in the password. So if you have only the same letter in one very long password, the strength is going to be low.
And then it verifies that there are uppercase letters, lowercase letters, and etc. It gives a rate, and then you can put the bar where it should be. And the last thing is something that is managed by the browser. So browsers accept a header that is called clear site data. And then you can tell the browser to clear, for example, cookies or the cache or the local storage when, and the typical use case is when people log out from a sensitive website. So if you develop a banking account website, then you certainly would like to leverage that so that then if this is used in the cyber cafe, then people can't go to read the bank accounts by just pushing the previous button on the browser.
Okay, more things on dependency injection. You know, parameters in Symfony, they help with passing simple values around. And we figured out that we missed a way to specify that a parameter should be used only at compile time. Maybe, you know, that Symfony is a compiled application. A Symfony application is a compiled application because the dependency injection container is something that needs to be first generated as PHP code and then used for every other request. And there are parameters that are useful only at build time. But because of some limitation, we couldn't do that. And now we do. What we created is a convention saying that if a parameter name starts with a dot, then it means it's a compile time only parameter so that you can then remove that in order to move existing compile time only parameters to this new system.
We added a way to deprecate a parameter so that we can say, oh, this full parameter is deprecated, please use dot full. Right. And other related, we also added an attribute named as alias that can be used. So if you have a class that is doing auto discovery to register a class as an alias. So if you have many classes implementing the same interface, you can label the one that should be used as an auto wiring alias. So that's going to be the default implementation of this interface when you need that. Of course, you can also use that to replace an existing service in Symfony or provided by a bundle. So if the implementation of the router is not good for you, you want another one. You can use as alias to replace the correct name to replace the router by yours. Dependency injection still. Now you can use, I mentioned before, auto configure lazy true.
You remember that this is about generating lazy proxies. Internally, the process is generating a class that provides the laziness. Before that, we used to rely on a third-party dependency. That's not the case anymore. Symfony has the code internally now to generate lazy proxies out of the box. So basically, it just works. Whereas before it was an optional feature. And because we have the code now, it was easier to iterate and to improve on this feature. So now you can use auto wire lazy true. And this is on an argument. And if you use this auto wire attribute lazy true on an argument, it means that your class is telling that this dependency with the attributes should be lazy. But maybe another reason is that this dependency should be lazy. So the other class is going to use the same service, but in a non-lazy way. And then Symfony will give the first class, the proxy one and the other one, a class that is not lazy because it didn't ask for that. So it moves the responsibility of declaring the laziness from the service that should be lazy to the consumption point, which is also nice.
There are many use cases for that. The last one is an improvement overall, which is a way for Symfony to generate a callable when needed. Let me show you an example. So let's say you have this constructor of one of your services. So, this is all you need to do to wire an implementation of, let's say, a UI expander interface. In this example, I'm making the assumption that this interface is in your domain, in your class, in your code. The URL item plate in the attribute is now a third-party dependency that doesn't know about the interface. And what you want to do is to take this expand method on the URL item plate class and turn it into an implementation of your IExpander interface because this interface has only one method, which is named expand, for example.
So what Symfony will do is that it will first generate a closure out of the URL template service. Doing this kind of call internally (you know, the three dots mean turn this method call into not a call, but a closure). So this is a closure and then generate the boilerplate to turn that into an implementation of your IExpander interface. If you look at this code, it's an adapter. There you see the argument of the constructor is the closure. And then we have a method expand that is declared in the interface. The implementation is about calling the closure and passing it all the arguments. So there you see Symfony in two lines allows you to save all this boilerplate. It's the domain of functional programming and it allows nice decoupling between your interfaces and third-party implementations.
On the development side, for the developer experience, now you will see that Doctrine migrations for sessions and locks are automatically created. You can also set private services during tests, which was possible before but using a hack; now it's much easier. And I'm sure you know dd (dump and die), everybody does dd. And now you can use named arguments for that. This is an example of setting in a test case the HTTP client, but a mock one so that then your application won't hit the network during this test, it will just use the mock. This is dd and its corresponding output when using named arguments.
Various goodies: We added support for UID version seven. If you don't know about that, it's the version to use for primary keys. If you use UIDs for primary keys, usually, you might be using version four. Historically, people were using version four. This has drawbacks. And version seven is the new version of the RFC that is not published yet. But the draft is almost finished and it has a fixed definition for what UID is and how UID should be generated on the request object. There is getPayload, and this will return an array, which is the JSON-decoded payload if the incoming request is a JSON one or the array of form submission contents. So use that to abstract either JSON or form inputs. I noted one more thing, which is about better signal handling in comments, which is a big article. There are so many things I didn't select in this talk that I can list them all.
So sorry about that. I'm sure there are some useful things that I couldn't list, but I'm running out of time. Let me continue. And everything I mentioned has been contributed by many, many people, not only me, not only the core team, but many more people. So I listed 360 people in the last six months. Over one year, it's about 500 unique people. So that's a lot. Thank you, everybody, for that. I'd also like to thank backers. If you use Symfony, you know Symfony is open source. Open source means free to use but not free to build. And there are many companies that understand the difference and basically sponsor Symfony. If you want to sponsor Symfony, please reach out to me. Thank you to every company listed there. You do help Symfony finance its development. Of course, there is also the community.
I invite everybody to join me and the rest of the community on Slack. You can go to Symfony.com/Slack. That's the main entry point to the community, apart from, of course, the GitHub repository. There you will find Mutual Aid, a support channel. It's really nice, with a lot of kindness and a welcoming atmosphere. There is also a care team, which is responsible for enforcing the code of conduct. We need a code of conduct to know how to react when some people don't behave exactly as we'd like the community to behave—being polite, not aggressive. Sometimes, of course, there are people that go off-road, and it's nice to have a process to let them know that this is not the standard we are expecting on this Slack, and that they should be more gentle. Thank you very much.
I think you learned; I hope you learned a few things. I also hope to see you in Brussels if you can come there or online in January. Brussels is in December; online is in January. I hope to see you at any other conference. Of course, I really miss meeting you. I have some friends in Ukraine, and I miss them a lot. So I hope we'll be able to see you all soon, as soon as possible, of course. Thank you for listening.