Writing a library for Symfony and Laravel at the same time.


Framework agnostic programming

Since I learned most of architecture from Symfony projects I do have the desire to always write my own libraries framework agnostic. You make a library e.g. pjordaan/example-lib and make a bundle to make it connect with Symfony, for example pjordaan/example-bundle. If I would make a Laravel binding I would create a pjordaan/laravel-example.

I noticed when developing the Apie library that this setup is very time consuming with larger libraries especially if you want to update your library but still make it possible to make a Symfony and Laravel bindings. I could solve some of them to make the Apie library a monorepo, but there were still a few problems in keeping the code maintenance as low as possible.

Problems with framework agnostic libraries

  • The binding library is very tightly coupled with the actual library. If I make a change to the library it also means I need to update a Bundle for Symfony and a Service Provider for Laravel
  • Service registry has no PSR, so I end up having to write the service registration twice as Symfony bundles and Laravel service providers are not compatible at all.
  • Symfony and Laravel have very different testing frameworks for integration tests. In general I do not want to write 3 integration tests(witout framework, with Symfony and with Laravel) all the time for every new feature.

How I solved the issues

I have a solution for the testing, but so far, nothing has been done here. The other problems I did already solve.
  1. Write all your code in a monorepo. Symfony and Laravel and also Google program their code in a monorepo so any BC-break or mutual upgrade can be done at the same time. I will probably write an article about how I set up the monorepo as it was not trivial.
  2. I first wrote the Symfony bundle and placed all services from a library in their own services.yaml file. The next step is moving the actual services.yaml outside the library inside the package and wrote a custom FileLocator to find the services.yaml. Any Symfony 2 wrapper is still in the bundle package and also has its own services.yaml often prefixed with 'sf2_'
  3. I made a new package outside the monorepo called apie/service-provider-generator that tries to read the services.yaml and create a service provider for Laravel.
  4. I added a shell command that uses apie/service-provider-generator to create Service providers for Laravel and placed them in the actual package.
  5. I made a package apie/common in the monorepo for any logic shared between apie/apie-bundle and apie/laravel-apie
  6. I made apie/laravel-apie with a service provider and used $this->app->register to conditionally register service providers.
And with 'little' effort I solved all of them except the testing, which I will try to figure out in the future to solve.

Making the service provider generator library

There were several solutions how to make this work, but in the end I decided to parse the yaml myself and just make assumptions of the structure of the services.yaml. I just had to find the differences between Symfony and Laravel to make sure the behaviour stays the same.

Default service binding

Laravel has automatic wiring, but I ignored this and specifically register all services in the service provider. Symfony has automatic wiring too with some configuration, but I decided not to add support as it is considered bad practice to autowire your services in a bundle.

Since Symfony always has one instance of a service, we make the service provider use $this->app->singleton to have always one instance of a service in the application.
Factories are also converted in PHP code and registered normally.

Optional arguments are mapped by checking $this->app->bound() to see if the service is already registered.

Configuration parameters

Laravel has config() to read a configuration value and Symfony has a very elaborate string parsing for
handling configuration. I ended with a trait with common functionality and a method that tries to parse the string and returns a value. It also has some basic support with environment variables, but support in Laravel is very limited in environment variables.

Tagged services

Symfony components use a lot of chain of responsibility and maps classes with a specific interface with a tag. Often the order of execution is important, so services can not only tag, but also add a priority. This function does not exist in Laravel and with $this->app->tagged() we can not even know what the original service id is, so a service locator is not possible at all.
In the end I ended up doing my own bookkeeping separately.

Conclusion

Even though we do have lots of PSR standards and Laravel and Symfony both use Symfony components, making something that is maintainable to have support for both was hard. By having our own wrapping logic we could maintain the library in 2 libraries and maybe even more in the future.

Links

Comments