When developing open-source packages, my aim is to make them highly configurable. This approach empowers the end-user to tailor the package to their specific needs, negating the need to fork the repository and maintain their own package version. This strategy also saves me from having to deal with feature requests that I have no desire to implement or maintain. In this article, I'll demonstrate how I create configurable packages for Laravel, and provide guidance on how you can do the same.
The initial step involves creating a configuration file for your package. This file will encompass all the configuration options for your package and will be published to the application's config directory upon package installation. The configuration file should carry the same name as your package and should reside in your package's config directory. For instance, if your package is named themis, the configuration file should be labeled themis.php.
The configuration file should return an array of configuration options. These options should be grouped by their purpose and thoroughly documented using comments. Below is an example of a configuration file for a package named themis:
Given this configuration file, the end-user can customize the package according to their needs by adjusting the configuration options. For instance, if the end-user wants to use a different table to store permissions, they can alter the tables.permissions configuration option to reflect the desired table name.
Similarly, they can modify the package's models by changing the models.permission and models.role configuration options to the class names of their preferred models.
Even the column names utilized by the package can be altered. This can be done by modifying the columns.model, columns.role_id, and columns.permission_id configuration options to their preferred column names.
Now, armed with this configuration file of known structure, we can develop an interface with methods that return these configuration options. This interface will serve the package by retrieving these configuration options. Here's an example of an interface for the themis package:
The interface is straightforward, featuring a method for each configuration option, all returning the options as strings. It is now ready to be implemented by a class that will extract these configuration options from the configuration file. Here's an example of a class implementing the interface:
The class implements the interface and fetches configuration options from the configuration file using the Config facade. It also includes a private method that retrieves configuration options and throws an exception if the configuration option isn't a string. The class is now prepared for registration in the service container. Here's an example of a service provider that registers the class in the service container:
Lastly, we need to create a facade for the class, enabling the end-user to access it conveniently. This can be achieved by crafting a facade class that extends the Illuminate\Support\Facades\Facade class and overrides the getFacadeAccessor method. This simplifies the end-user's access to the configuration. Here's an example of a facade class for the themis package:
The facade class extends the Illuminate\Support\Facades\Facade class and overrides the getFacadeAccessor method to return the class name of the class that implements the interface. The facade class also includes a @method annotation for each method in the interface. This annotation simplifies usage of the facade class for the end-user and facilitates code completion within the IDE.
The End
That's it! You've now created a configuration that's both easily accessible and type-safe, ready to be used by the end-user. The end-user can interact with this configuration via the facade, eliminating the need to inject the ConfigurationInterface instance or resolve it from the container. All the end-user needs to do is utilize the facade class, and the configuration options will be readily available.