Search code examples
phpsymfonyarchitecturebundle

How should 3rd party bundles be wrapped in Symfony2?


If I add a 3rd party bundle, say from Knp bundles for example, should I wrap it first or should I use it directly in my code?

If I decide to wrap it, where do I put the wrapping code? In a separate new bundle? In my application bundle?

To clarify:

I'm not asking about how to add a thirds party bundle to my project. I'm not asking what a bundle is.

This question is aimed at encapsulating 3rd party code behind wrapper classes. Since the bundle was developed by a 3rd party developer it is a subject for unexpected changes that could break my code.

How do you wrap a 3rd party bundle after adding it to your project?


Solution

  • This is an answer for 3rd party bundles included via composer in Symfony2 in general, it does not refer to a special bundle.

    First of all

    As long as you are fixing the version of the requested bundle to a stable version (like 1.*) in your composer.json (and as long as the developer follows his own guide lines), you shouldn't have any problems with compatibility breaks of the interfaces, thus wrapping is not necessary.

    But I'm assuming that you want to prevent any code breaks by throwing Exceptions in the wrapper code and/or implementing fallbacks, so that everything that uses the wrapper code could still work or at least display appropriate errors.

    If you want to wrap

    If you want to use the dev-master version of a given 3rd party bundle, major changes might occur. But there shouldn't be a case where you really want to include the dev-master when there are stable versions.

    Anyway, there are two ways that I see, that could make sense in the case that you want to include the dev-master or want to wrap it to display errors, log them, catch exceptions etc.:

    Build a single service class that uses all instances of the services of the 3rd party bundle

    This service class could be in one of your bundles that uses the 3rd party bundle, there's no need for an extra bundle in this approach.

    This way you have a single service like acme.thirdparty.client that wraps single method calls of other services. You would need to inject all 3rd party services that you need (or create instances of the desired sub classes) and wrap all desired method calls.

    # src/Acme/MyBundle/Resources/config/services.yml
    parameters:
        acme.thirdparty.wrapper.class: Acme\MyBundle\Service\WrapperClass
    
    services:
        acme.thirdparty.wrapper:
            class: %acme.thirdparty.wrapper.class%
            arguments:
                someService: @somevendor.somebundle.someservice
                someOtherService: @somevendor.somebundle.someotherservice
    

    And the service class:

    <?php
    namespace Acme\MyBundle\Service;
    
    use SomeVendor\SomeBundle\SomeService\ConcreteService;
    use SomeVendor\SomeBundle\SomeService\OtherConcreteService;
    
    class WrapperClass
    {
        private $someService;
    
        private $someOtherService;
    
        public function __construct(ConcreteService $someService, OtherConcreteService $someOtherService)
        {
            $this->someService = $someService;
            $this->someOtherService = $someOtherService;
        }
    
        /**
         * @see SomeVendor\SomeBundle\SomeService\ConcreteService::someMethod
         */
        public function someMethod($foo, $bar = null)
        {
            // Do stuff
            return $this->someService->someMethod();
        }
    
        /**
         * @see SomeVendor\SomeBundle\SomeService\ConcreteOtherService::someOtherMethod
         */
        public function someOtherMethod($baz)
        {
            // Do stuff
            return $this->someOtherService->someOtherMethod();
        }
    }
    

    You could then add some error handling to those method calls (like catching all exceptions and log them etc.) and thus prevent any code outside of the service class to break. But needless to say, this does not prevent any unexpected behaviour of the 3rd party bundle.

    or you could:

    Create a bundle that has multiple services, each wrapping a single service of the 3rd party bundle

    A whole bundle has the advantage of being more flexible on what you exactly want to wrap. You could wrap a whole service or just single repositories and replace the wrapped classes with your own ones. The DI container allows the overriding of injected classes, like the following:

    # src/Acme/WrapperBundle/Resources/config/services.yml
    parameters:
        somevendor.somebundle.someservice.class: Acme\WrapperBundle\Service\WrapperClass
    

    By overriding the class parameter somevendor.somebundle.someservice.class all services that use this class are now instances of Acme\WrapperBundle\Service\WrapperClass. This wrapper class could be either extending the base class:

    <?php
    namespace Acme\WrapperBundle\Service;
    
    use SomeVendor\SomeBundle\SomeService\ConcreteService;
    
    class WrapperClass extends ConcreteService
    {
         /**
          * @see ConcreteService::someMethod
          */
         public function someMethod($foo, $bar = null)
         {
             // Do stuff here
             parent::someMethod($foo, $bar);
             // And some more stuff here
         }
    }
    

    ... or could use an instance of the original class to wrap it:

    <?php
    namespace Acme\WrapperBundle\Service;
    
    use SomeVendor\SomeBundle\SomeService\ConcreteServiceInterface;
    use SomeVendor\SomeBundle\SomeService\ConcreteService;
    
    class WrapperClass implements ConcreteServiceInterface
    {
        private $someService;
    
        /**
         * Note that this class should have the same constructor as the service. 
         * This could be achieved by implementing an interface
         */
        public function __construct($foo, $bar)
        {
            $this->someService = new ConcreteService($foo, $bar);
        }
    
         /**
          * @see ConcreteService::someMethod
          */
         public function someMethod($foo, $bar = null)
         {
             // Do stuff here
             $this->someService->someMethod($foo, $bar);
             // And some more stuff here
         }
    }
    

    Note that implementing an interface for a class that is overriding another one might be mandatory. Also the second one might not be the best idea, since then it's not very clear that you are actually wrapping the ConcreteService and not just replacing it. Also this is ignoring the whole idea of Dependency Injection.

    This approach needs a lot more work and means a lot more testing, but if you want more flexibility, this is the way to go.

    Perhaps there already are wrapper bundles for your desired 3rd party bundles around (like the SensioBuzzBundle for the Buzz Browser), in this case, you could porbably use those instead of writing everything yourself.

    Conclusion

    Trusting the developer and including a stable version (like 1.* for bugfixes and new features or 1.0.* for bugfixes only) is the way to go. If there are no stable versions or if you want to include the dev-master, wrapping is an option. If you want to wrap your code, building an extra bundle is the more flexible way, but a single service class could be enough if there's not much code to wrap.