Good PHP package design
Over the 7 years I have writte a lot of open- and closed-source packages in various programming languages. Most of them were written in PHP, but I've also spent a couple of years writing Objective-C and Java packages. But the days that I've written Objective-C or Java are long gone, so this post will focus on PHP. But most of the techniques and design principles can also be applied to any other programming language.
Why create packages in the first place?
When I publish my code as open-source, my main motivation is to create reusable pieces of code and document the package usage and logic for myself. If other people will later come and find one of my packages and use them in their own applications, that is an added benefit - but it's never the main motivation for me to write open source software. The same can be said when writing packages in a closed-source environment.
So what exactly is the goal of a a package?
In my opinion, splitting larger code bases into multiple reusable pieces of packages solves a couple of problems.
- Make your code reusable across multiple projects
- Document your packages
- Fix generic bugs once and apply them to multiple projects at once
- Simpler code
Make your code reusable
How many times have you browsed old codebases of yourselve or of one of your colleges to search for a solution to a specific problem that you were working on? It happened to me quite a lot of times. And most of the times, the solution that was implemented in some other project was of course not generic enough to just "plug and play" it into another codebase. Maybe the solution was framework-dependent and the framework of the current codebase was a different one from the framework that the solution was being used in. To be honest, since I am working in Laravel projects for 99% of my time, this is not an issue that affects me too much. But it's still a very valid point to have.
The next thing I noticed when searching for other pieces of software in older projects was that most of the times these older solutions were very much tied to the domain logic of the application that it was built into. And that made it also harder to just reuse the code in a different project. Even if it solved the same problem.
Let me give you an example of a PHP package that I have recently built - and how a similar package would've looked like without good package design.
In one of the applications that my company Beyond Code is currently working on, we needed the functionality of creating vouchers and associate them to domain specific objects - in our case Laravel Eloquent models.
For example, let's say that you are selling multiple video courses and want to create a voucher that will give away one specific video course for free, when applied - but it will not affect other video courses in your system. And when a user is going to apply this one voucher, we needed to apply some domain specific logic to associate database entries, send out emails, etc.
Finding a solution
Now when you just take a look at the problem, the "easiest" or most straight-forward approach might be to simply add this voucher logic into your application and tie it into it. But that's one of the problems that we want to solve. Because the next time that we need to add some kind of voucher to a project, I want to be able to just use one of my existing solutions and apply it to the business logic as I see fit.
So how do we avoid a specific implementation but rather keep things more generic?
I bet in your software development career - no matter at which point you are right now - you've probably heard the term "SOLID principles" before.
Let's go briefly over these principles:
- The Single Responsibility Principle
A class should have one, and only one, reason to change.
Now there is a common misconception about this when designing classes, that this principle means that every class should only have one responsibility. But the term responsibility does not need to be applied on a code/low-level, but should rather be seen as what the class actually does - for example when something conceptually becomes different for a specific class.
- The Open Closed Principle
You should be able to extend a classes behavior, without modifying it
So this basically means, you should not need to edit a class if the change you need to make is not related to this specific class.
Let me give you a simple example:
You have a class that stores some data in a cache. Now if you want to switch from a file-based cache to, let's say, Redis, you should not need to edit the Cache class for this change to happen.
- The Liskov Substitution Principle
Derived classes must be substitutable for their base classes
or to make it even more complex:
Substitutability is a principle in object-oriented programming stating that, in a computer program, if S is a subtype of T, then objects of type T may be replaced with objects of type S
This is one of the principles that took a long way for me to actually click. So here's my example of illustrating what this means. When designing your code you should be able to swap out classes that share the same "type" / inheritance without breaking an actual implementation.
So let's say you need to model two classes. A
Rectangle and a
Square. Your first idea might be, to have a
Rectangle class and then create a
Square class that extends the
Rectangle class. Because from a mathematical standpoint a
Square is a
However this would result in some strange behavior when you swap out a Rectangle implementation with a Square implementation.
Imagine you have a
setWidth and a
setHeight method on your
Rectangle class. For a rectangle this makes perfect sense, but if you use a square, whatever method gets called last will override the value of the other - since height and width are the same on a square.
- The Interface Segregation Principle
Make fine grained interfaces that are client specific
This principle is related to the single responsibility principle and basically means that if you provide interfaces in your codebase, they should be kept rather small so that classes implementing these interfaces only have to implement the minimal subset of methods required for this specific task.
- The Dependency Inversion Principle
Depend on abstractions, not concrete classes
This means that when your code takes some dependencies as arguments, these dependencies should be abstract - for example with an interface - and not the concrete implementation, to make it very easy to swap out the actual implementations being used.
Phew...okay that's what SOLID means. So as the name suggests, these are principles - not rules. Do not stress yourself, especially when you get started with package design, about these principles too much. You will learn along the way! But it's good to know what you should try and keep an eye on while building your package - and your classes nontheless.
Putting things into practice
As I mentioned, the package that I have worked on is framework dependent - mostly because Laravel is the framework that I am using ~99% of the time and it allows me to add additional niceties to my packages. Another approach would have been to release (and maintain) two packages - one framework agnostic version and then a Laravel specific implementation that makes use of the framework agnostic code.
Make things configurable
In the example of the voucher generation the package is also capable of generating voucher codes. In my client-specific use-case I would later need a voucher that has a specific prefix or suffix. So I made these things configurable in a config file.
Of course I could have also created a VoucherGeneratorInterface, created my own VoucherGenerator implementation and then make this specific implementation configurable, but I felt like this is not what I would need after all.
Instead I just have a configurable prefix, suffix, mask of the voucher and a list of randomc characters that can make it into a voucher code.
This way I could remove possibly confusing characters that might look the same. For example:
No business logic at all
To make the voucher package applicable to my current client project, as well as possible future applications, I do not want to implement any kind of business logic that will happen when a voucher gets redeemed, other than storing a
redeemed_at date on the voucher model as I think that this will be useful for all voucher implementations.
Instead I simply make use of Laravels excellent event system and emit a custom
VoucherRedeemed event, that holds the voucher class as well as the actual eloquent model that this voucher was redeemed on.
This logic is put into a trait, so that the developers consuming my package, could simply add this trait to the eloquent models that will be able to redeem vouchers.
The resulting code
And here is the basic Laravel specific code that I came up with:
// Create a voucher for one video course entity $videoCourse = VideoCourse::find(1); $voucher = $videoCourse->createVoucher(); // A user can redeem the code $user->redeemVoucher($voucher);
Since my voucher package now does not have any business logic attached to it, testing becomes really simple. Because all I need to test is the minimal logic of this package itself. And when you take a look at the existing tests you can clearly see that the tests are very easy to follow. Also Laravel's Event fake mechanism makes testing the framework dependent code super easy.
Similar to how easy it becomes to test a smaller chunk of code, it also becomes a lot easier to write documentation for a smaller piece of code. So if you think about the Single Responsinbility Principle again, my package now has a single responsibility - creating, validating and redeeming vouchers. Now even if you are not a big fan of writing documentation, it simply becomes a lot less work than writing documentation for a full-blown application itself. So here's the documentation of the Laravel Voucher package as an example.
Where to go from here?
This blog post only covers some aspects of the actual process of writing packages and there is a lot more to it when you actually go and build a package yourself.
To give you an idea of what you have to cover. here's a (probably incomplete) list of tasks that you need to take care of at some point.
- Using git for version control
- Define the package to use it with composer
- Host your package somewhere
- Use correct auto-loading for your package
- What are the required dependencies of your package itself?
- What are the development-only dependencies of your package?
- Correctly version your package using SemVer
- Create proper releases
- Add automated testing and quality control tools
And at this point, you have not even open sourced this package.
Now there is no need to be afraid about all of this, as it might seem like a lot of things to grasp, but once you've understood the basic concepts, it becomes really simple.
PHP Package Development Course
When I first started creating PHP packages, so only good resources available were other open-source projects. There is no simple tutorial or video course that covers not only the package creation, but also things like application pre-requesites and all the other tasks that come with creating, publishing and maintaining packages - whether or not it's open- or closed-source.
For this, I have started working on a new video course called PHP Package Development that is set to be released in early 2019 and show you how to create your own reusable PHP packages for yourself, your company or for the whole world on GitHub.
If you are interested in learning more about PHP package design, be sure to sign up and get notified when the course launches, as well as receive a launch discount code.