Proven Package Patterns
In the age of AI, packages still earn their place for hard problems, external problems, and tasteful solutions, but maybe not for the trivial shared ones an agent can write inline for your project.
Transcript
Freek is one of the co-founders of Spatie, which if you look in your composer.json, you'll understand who Spatie is. Their packages have uh over a billion downloads. Um but not only do they have a whole bunch of packages, they have a whole bunch of products now. So, they've got Oh Dear, There There, Flare, Mailcoach, stuff that we all use, stuff that we all love. Um and he's also a very prolific blogger at freek.dev. And so, he is here, of course, to talk to us about proven package patterns. Please welcome to the stage Freek.
>> Here we are, another Laracon. How have you been enjoying yourself so far?
>> [cheering]
>> Yeah. Love being at Laracons. Okay, let's start with the talk. So, for those that know that don't know me, I'm Freek. I'm a partner and developer at Spatie. Like Aaron said, I have a blog, freek.dev. I send a bi-weekly newsletter. Uh and you can find me on all social channels under my nickname uh Freek Van der Meulen. Now, before heading into the talk, I want to say a few words about the things that uh Spatie does. Um we craft web application software and open source in Laravel ecosystem. We've been around since 2009. I think we switched to Laravel in 2013, 2014 from Zend to to Laravel, which is like one of the best decisions that I've ever made. Um you might think that Spatie is very big, but we're only a uh a team of uh 12 passionate people. Uh we usually work in uh Laravel, React, Tailwind, Livewire, and all related technologies. We also use AI, of course. And the thing that we're also very good in uh good in is research and analysis. Before we code, we really want to understand your problem and know what we uh should do. Now, uh we work on a couple of sauces. First one is Flare, which is air tracking and performance monitoring for Laravel apps. I also work on Mailcoach, which is an email platform for your entire marketing needs. With my buddy Mathias, I work on Oh Dear, which is battery included uptime monitoring service. We don't only monitor your homepage, but we will actually crawl all the links and we will notify you when one of the pages is down. We also monitor DNS, scheduled jobs, ports, you name it. It's batteries included. And one thing I'm really now passionate about is There There, which is a newish kind of product. We still have to launch it formally, but I've been working on it for a couple of months and we use it internally. It's a helpdesk where AI helps you get all of the information you need to send a good answer to your customer and we use this to do customer support for all our other projects. Um so, Spatie has products. We're web dev consultancy, but you probably know us from our open source packages. You'll find a big list of everything that we have on our open source pages and there is also like a story and all the values we have when creating open source on that page. Now, I guess many of you knew this already because these are the download numbers per year cumulatively. You can see that we're now over 2 and 1/2 billion downloads and that's still growing and I think that's mighty fine with a team as small as ours. Um Now, why are these Spatie packages so popular? And there are a couple of reasons and I think the first reason is developer experience. They're easy to use. Your AI can also use them easily. They have excellent documentation, good tests, and we keep them up to date. Some of these packages, they started in the Laravel 4 days and we still support them for the latest features of the framework. I think they're also popular because of their design. Most of them are small in scope. They just solve one problem really good. They also have readable code and a flexible structure. They are really easy to to customize and I'm going to talk about that part a little bit later. But I'm pretty sure that a lot of you are thinking in this AI age, do we still need packages? And that's Spatie because yeah, we have stakes in this. We talked about this and we come up we came up with a couple of categories, which I'll talk about one by one. First up, the shared problem. There are packages that yeah, try to fix or to solve a common or very easy problem and that problem is well-defined and it doesn't evolve. For instance, generating menus, breadcrumbs, or deciding which menu item is active. We have a package for that called Spatie Laravel navigation and the package has a very nice API, but if we're honest, AI can just build this for you in your own project as well, tailored to your own needs. This isn't a very complex code, so maybe the shared problem packages aren't as needed as they used to be. Then we have packages that try to solve a hard problem. There's some problems, they seem simple, but they are not. Take for instance, the calculation of business hours. Maybe you want to you have like a global shop and you want to show opening hours of your stores on your application and you need to decide, are we open or are we closed? And it starts easy. You have Monday to Friday, 9:00 to 5:00, but it could get complex with holidays, closing days, special Sunday openings, and then the programmer's favorite time zones. And sure enough, you could uh solve this with AI, but you need to describe all of these special cases to AI and you need to take ownership of what the AI produces. And I think in such cases, a package can still be handy, so you don't need to focus or take ownership of that hard problem, and you can focus on your core business instead. And by the way, we have packages for opening hours and for international holidays. Now, the third category is the external problem. Some problems, they are tied to just something outside of your control. Think of APIs, services, and protocols. And these things, they can evolve over time. And well-maintained packages can absorb that maintenance burden. I think Prism is a good example of this, where you don't need to maintain it or need to know the special cases. The Prism package can do that. And you can reuse that across projects. I think if you only use an AI like in a small way, then you can let just AI just generate a single endpoint of you. But if you use it extensively, a package for an API can still be very helpful. And then the last category, which is I think my favorite category, is the tasteful solutions. Cuz I think that not all packages, you should consider them as like a dry solution to a technical problem. Some of these packages are very opinionated and well-crafted. Examples of of these are Inertia, Livewire, Spatie, our own media library. And I know it's not a package, but you can see Laravel in this way as well. And I think AI could build light versions of these packages easily. Like, for instance, Livewire, I bet AI could come up with like some sort of component system, but I don't think it will add like a very beautiful way of describing it and of adding options like what Caleb has done with Livewire. I think in general that these packages those authors, they just have taste and tasteful authors, they write tasteful solutions and in the meanwhile, they also solve hard problems under the hood as well. So, I think that for foundational stuff, you should choose tasteful solutions because AI doesn't have like the right taste for solving these kind of problems in a very nice way yet. And in this talk, I'm going to give you a few small taste for a few small tips, I mean, for creating readable and flexible code. And these are things that we've doing them for years. I'll show you them in a package context, but this applies to projects as well. And I'm going to use our own Laravel URL URL AI Transformer package for this. This package is a newish kind of package. I think I created it half a half a year ago and it does something very simple. It can just read the content of a bunch of URLs. It can transform that content using AI and then you can store it in the database for later use and you can schedule that transformation whenever you want. So, if you want the transformation to run daily or weekly, you can just do it because it's just Laravel, right? Now, why did we create this package? This is yeah, the use case the the actual use case why I created it. We wanted to add LD plus JSON data to all of the pages of our company website. And if you don't know LD plus JSON JSON, that is just a structured data to help crawlers and AI understand what is on the on the page. For instance, here on the on the right, you can see this is a website for a company named Spar C. This is This is what they do. These are the things you can do on on that website. And we didn't invent this. This is actually a standard called JSON-LD.org. Now, let me show you a little demo so we all know what this package what this package does. So, if I Maybe I should first say that I created a little demo application here. It's just a simple blog with a list of of articles and a detail page. And here on the left we have the actual source code. And you can see here that we have a function in the app service provider. We first fetch all the article URLs and you can replace this with some kind of crawler function if you want to, but I wanted to keep it simple. Just give me the article URLs. You can give that to the transform facade to the URL functions and you can say, "Hey, transform these URLs using this this transformer." So, that LD.json transformer, that's this class and that's basically just a prompt to say, "Hey, use the HTML that we scraped and transform it to LD.json." And it will do that and store that inside of the database. Now, I wanted to do this on stage, but I saw like with Nuno it took like 40 seconds. I don't know any good jokes, so I just did it did it beforehand. So, this is the result what is being saved in the transformation results page. It is just the URL, the type, which is determined by the transformer here. And you can see that the result is that LD.json snippet. If I go to the demo application and I go to um a detail page. You can see I rendered here that ld.json, but where it should definitely be is in the head section. If I open the script here up, then you can see the ld.json here. If we go to the show view here, yeah, do you still remember opening an editor and looking at code? If you look here, then you can see the article has an ld.json function. And if I go to the model here and I go to the ld.json function, you can see here that we use the model provided by the package transformation result and a little helper to to get that result. And that's basically everything that the package can do. It's simple, but it's helpful. Okay, back to the back to the slides. So, whenever we create a package like this, we get questions like this for people who want to tailor it to their own needs. And we get questions like, is it possible to lock when these transformations run? Or my database setup is very special. Can we use another database connection for that one model? Or could you add an option to let the transformation job run on another queue because their queuing system is a little bit complicated? Or something that goes about like the core core logic. When getting content from my side, could we just add some extra headers there? Maybe to get into an authorized section where the application where you want to fetch a result from just can check like if there is like a secret header there, I'm going to going to pass through. That's a question that we get a lot with packages like this. Okay, let's go to all of these questions one by one. Is it possible to lock when these transformations run? And sure enough, we could add an extra option to this in our config file. Lock when transformation run, set it set it to true. And then we could just use that configuration option in the package and just try to log something. And then the package could do the transformation and then at the end it could log transformation ended and then the the type of transformation that that run. People could ask could ask questions like, "Hey, can we just customize the message that is logged?" And sure enough we could add config options for that as well. And we could try to use those config options as well. And you can see we could keep on adding options, but yeah, that just isn't the way to go. You feel like the code's pushing pushing back. This doesn't work because the code gets This is a simple example, but with complicated stuff having a lot of options that gets difficult to manage very fast. And having too much small options for details like this is very confusing and as a package container maintaining or reviewing this kind of kind of lodging certainly isn't fun. And there is a very simple way of of handling this. You already know it. You can just use events for this. So you can just fire off an event in the package, transform something, and fire an off event again. And then package users can do whatever they want when they when they listen for that event. And all of those separate config options, they aren't needed anymore. This is a very simple example, but I have a couple of more where we drive this a little bit where we drive this theory a little bit better. So second question, my database setup is very special. Can we use another different database connection? And sure, we could add an extra option in our config file, model connection name, but you already see the the pattern here emerging, I think. We could use that option inside of the the the the package code. So, in our model, we could just use the config value here. Uh and of course, other options would get requested because sometimes people want to have another table name or use soft deletes and stuff. And so, a better way to handle this is just by letting your users just specify the model that needs to be used by the package. So, in our default uh configuration, we just add the default model that gets used by the package. And what users can do is just extend the default model with a custom one of their own, and they can just add any attributes of traits they they want on it. And they can specify their custom model on it. And now, you have uh custom behavior inside of the package without me having to um um come up with all of these these separate options. And of course, as a package author, you can't use the model itself anymore, but you should use the one from from the model. And yeah, this can be used to let users configure table name, connection name, add traits, and modify functions. Next question. Can you add an option to let the transformation job run on another queue? Yeah. We [snorts] could add an extra option to our config file, but we don't want to do that, right? Um what we could do is just allow users to specify the entire job class itself, like we've done with the model. And then, people can just, yeah, extend our default one with their custom one, add all the options they want on there. And people can just use that custom job in the config. And users can now modify the job however they want, custom queue names, timeouts, retries without me caring about how it's used. Okay, last question. When getting content from my site, could we please set some extra header requests? We're going to start with the solution that we could have done, adding an option to the config file, but we don't want to do that, right? Because that gets messy. So, this could be the code, as simple as that, how we fetch the content of a URL. Just use the HTTP facade. Now, what we do mostly in in both packages and projects, it just wrap it into an action class. So, an action class is just a verb, something you do, and then action. And most of these classes have just one single function. I like to use the word execute, but you could also make it make it invocable. And if you have that class or I should first say that yeah, we can encapsulate a piece of logic with with this. And it's also very good for testability because you can just test that one class in isolation. And this is yeah, a great way to organize code that we do in both packages and projects. Now, what we do in our packages is we add an action key to the to the config file, and then a key per action, and then the default action that is provided by by the package. And what users can do is then create their own custom function that extends our default one, and then you can just override stuff. And in this case, I've just added the extra headers here. And a package user can then specify that custom class here and then yeah, you can customize the behavior without me having to touch the package for that special case. So, action classes just wrap small pieces of functionality that you want to be overridden in the class, register them in the config file. This makes your package very, very flexible and yeah, you can use this in a project as as well, too. Now, inside of the if inside of the package, this is how yeah, we can use that. We can just uh get the class name from the config file. We new up the class and then we can just call execute on it and call the day. Okay, let's go back to the uh demo project and take a look at how our actual config file looks like and you'll see that it is very familiar. So, we have an uh AI key here to uh specify yeah, which AI that we want to use. But here you can see that we have a custom model, so people can override stuff. We have your actions for all low-level behavior here and we also have like a key for um specifying a custom a custom job. So, it makes the config file really pretty, really small uh and I don't have to have a lot of uh options uh in here. Let's go back to the slides here. Now, you might ask again in the age of AI, why is this uh important? Do I still have to to to know how to do this? And I want to present to you uh a tweet that Taylor did uh sometime ago. Um and it says, "When you write foundational architectural code of a new project by hand, you feel the code pushing back if the abstraction isn't right." And that's something that uh that stuck with that that yeah, that stuck with me. So, whenever you ask an AI to just keep adding options, if I would give like an AI all the questions that I had to my slide, it would just happily add all of those options. And the result would be like the AI programming like a config file with hundreds of options. And this isn't just like a theoretical thing. When I first created that package, I actually had that. I actually had like uh a lot of options in that config file. And I think for developers, if you feel that kind of friction, then that's a telltale sign that something is wrong, that you might solve it in a little better way. But if you use AI, you sometimes don't feel that friction cuz AI just doesn't feel friction. And AI, like I said before, it just doesn't have like the taste yet to do this. So, I think you need to know these patterns to direct AI in the in the right direction. Uh and there's also a good blog post about this. I'll tweet it out after my talk. So, let's recap what what we've said. These are if you should create a package and try to make it very extensible, these are good tips for you. Avoid adding the tiny options. I'm of the school of trusting the users. You're all grown-ups. You can all do what the package whatever you want. So, I like to give users full control. Fire events wherever needed so people can just hook in wherever they want. Uh allow registering custom models and jobs. Use those action classes and know your patterns so you can instruct AI to follow the right patterns. That's all I have for you. Hope you like the conference. See you next time. Thank you.
>> [applause]
>> So good. Hey, man. So, let me ask you a question about AI. Um
>> Yeah.
>> I agree with the thesis that you have to know the patterns, you have to feel the friction. Where are you using AI to still speed up? Is it configuring CICD? Is it tests? Is it after the patterns have been established when you're creating a package?
>> Uh when I'm creating a package, um of course, like everybody, I use AI. Um sometimes for scaffolding out a couple of the ideas, or maybe when I try to solve like a very technical problem, just give me like the very the simplest way of of solving this. So, that's at at the starting point when I when I create a package to just give me like a couple of good solutions for like this technical thing. Um and something that I I always take a lot of care of having like a good API. How can you use field How will users use this package? How What's What's the feel of it? And it used to be like a lot of tinkering. And with AI, I can just do like three or four directions in one go and just feel whatever feels best for me.
>> have it rip four different implementations and you kind of feel which one is best.
>> Maybe not implementations, but like APIs and just like what feels best.
>> Yeah. Yeah, I like that. Y'all give it up for Freek.
>> Thank you.
>> Good job.
>> Thank you.
Freek Van Der Herten