Sharing Your Extensions - Extending Symfony2 Web Application Framework (2014)

Extending Symfony2 Web Application Framework (2014)

Chapter 6. Sharing Your Extensions

Since everything is a bundle in Symfony, all the code you write is already in the structure it needs to be in order to be shared with others. If we take all the code that we wrote over the course of this book inside the BookBundle folder, and make it available to others, all they would have to do to make it work is copy our configurations. This is nice, but it is still a "lot of work" to do, which includes defining each of the services with the right parameters and so on.

In this chapter, we will look at the steps required to make an easy-to-use bundle for others as well as other best practices for sharing code. In Chapter 4, Security, we added a way for users to sign in using their GitHub account. This is a good example of something that others might want to reuse or that we ourselves might want to reuse from one project to another.

Creating the bundle

While developing our app initially, we didn't care about where our files were. Everything was under a giant monolithic bundle that included everything. We'll go through the following steps to change the situation and make a decoupled GithubAuthBundle:

1. Set up the bundle.

2. Move or write the code.

3. Move or create the services configuration directly in the bundle.

4. Define the bundle configuration and merge the user-defined parameters.

First, we will use the following command line provided by Symfony to generate an empty bundle:

php app/console generate:bundle

To do this, you have to choose a namespace and a bundle name, which in the case of this book are Khepin and GithubAuthBundle. Now, let's move all the required files to this new bundle and update their namespaces accordingly. In the end, our bundle structure should be as follows:

GithubAuthBundle/

DependencyInjection/

Configuration.php

KhepinGithubAuthExtension.php

Resources/

config/

services.xml

Security/

Github/

AuthenticationListener.php

AuthenticationProvider.php

GithubUserToken.php

SecurityFactory.php

UserProvider.php

Test/

KhepinGithubAuthBundle.php

Also note that the KhepinGitAuthBundle bundle class now needs to contain the code that was previously in KhepinBookBundle to register the security factory, as follows:

// Updated BookBundle class

namespace Khepin\BookBundle;

use Symfony\Component\HttpKernel\Bundle\Bundle;

class KhepinBookBundle extends Bundle

{

}

// GithubAuthBundle class

namespace Khepin\GithubAuthBundle;

use Symfony\Component\HttpKernel\Bundle\Bundle;

use Symfony\Component\DependencyInjection\ContainerBuilder;

use Khepin\GithubAuthBundle\Security\Github\SecurityFactory;

class KhepinGithubAuthBundle extends Bundle

{

public function build(ContainerBuilder $container)

{

parent::build($container);

$extension = $container->getExtension('security');

$extension->addSecurityListenerFactory(

new SecurityFactory()

);

}

}

Once we do so, our previously working code will stop functioning. All the services we had defined in our config.yml file are now referencing the files that are not there anymore.

Symfony lets us move the service definitions to the bundles themselves. This is what we will do, going from a YML-based configuration to an XML-based one. It is recommended that you use XML when creating a bundle to be shared with others instead of other forms of configurations (PHP, annotations, or YML) since the XML format is more flexible.

Our initial configuration is as follows:

khepin.github.authentication_provider:

class: Khepin\BookBundle\Security\Github\AuthenticationProvider

public: false

Now, the configuration is different, as follows:

<service

id="khepin.github.authentication_provider"

class="Khepin\GithubAuthBundle\Security\ …

… Github\AuthenticationProvider"

public="false">

</service>

The complete configuration of the file is as follows:

<?xml version="1.0" ?>

<container xmlns="http://symfony.com/schema/dic/services"

xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">

<services>

<service

id="khepin.github.authentication_provider"

class="Khepin\GithubAuthBundle\Security\Github\ …

… AuthenticationProvider"

public="false">

</service>

<service

id="khepin.github.user_provider"

class="Khepin\GithubAuthBundle\Security\ …

… Github\UserProvider">

<argument type="service"

id="fos_user.user_manager" />

</service>

<service

id="khepin.github.authentication_listener"

class="Khepin\GithubAuthBundle\Security\ … … Github\AuthenticationListener"

parent="security.authentication.listener.abstract"

abstract="true"

public="false">

</service>

</services>

</container>

Our bundle is now self contained, and no configuration will be needed to make it work in another project. However, you will still need to add the bundle to the AppKernel file, and set up the user provider in the security configuration.

Exposing the configuration

There is a problem with our AuthenticationListener class though. From where we left things in Chapter 4, Security, it contained the credentials for our GitHub application. We'll want our users to provide their own credentials instead.

The AuthenticationListener class is as follows:

class AuthenticationListener extends AbstractAuthenticationListener

{

protected $client_id;

protected $client_secret;

protected function attemptAuthentication(Request $request)

{

$client = new \Guzzle\Http\Client(

'https://github.com/login/oauth/access_token'

);

$req = $client->post('', null, [

'client_id' => $this->client_id,

'client_secret' => $$this->client_secret,

'code' => $request->query->get('code')

])->setHeader('Accept', 'application/json');

// ...

}

public function setClientId($id)

{

$this->client_id = $id;

}

public function setClientSecret($secret)

{

$this->client_secret = $secret;

}

}

We will update our AuthenticationListener class to provide two methods to set the credentials. We know that since our class inherits from an abstract class, the constructor methods within that class take many parameters and are already configured. We prefer to avoid messing with this as there is a risk of breaking compatibility if the underlying interface changes in the future. For this, we will inject the following arguments through methods instead of the constructor:

<service

id="khepin.github.authentication_listener"

class="Khepin\GithubAuthBundle\Security\Github\ …

… AuthenticationListener"

parent="security.authentication.listener.abstract"

abstract="true"

public="false">

<call method="setClientId">

<argument>xxxx</argument>

</call>

<call method="setClientSecret">

<argument>xxxx</argument>

</call>

</service>

Now, we want to let other users configure these values from their own config.yml file as follows:

khepin_github_auth:

client_id: xxxx

client_secret: xxxx

To do this, we will update the services.xml service definition as follows:

<?xml version="1.0" ?>

<container xmlns="http://symfony.com/schema/dic/services"

xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">

<parameters>

<parameter key="khepin_github_auth.client_id">

</parameter>

<parameter key="khepin_github_auth.client_secret">

</parameter>

<parameterkey="khepin_github_auth.authentication_provider_class">

Khepin\GithubAuthBundle\Security\Github\AuthenticationProvider

</parameter>

<parameter key="khepin_github_auth.user_provider_class">

Khepin\GithubAuthBundle\Security\Github\UserProvider

</parameter>

<parameter key="khepin_github_auth.authentication_listener_class">

Khepin\GithubAuthBundle\Security\Github\AuthenticationListener

</parameter>

</parameters>

<services>

<service

id="khepin.github.authentication_provider"

class="%khepin_github_auth. …

… authentication_provider_class%"

public="false">

</service>

<service

id="khepin.github.user_provider"

class="%khepin_github_auth.user_provider_class%">

<argument type="service"

id="fos_user.user_manager" />

</service>

<service

id="khepin.github.authentication_listener"

class="%khepin_github_auth.authentication_listener_class%"

parent="security.authentication.listener.abstract"

abstract="true"

public="false">

<call method="setClientId">

<argument>

%khepin_github_auth.client_id%

</argument>

</call>

<call method="setClientSecret">

<argument>

%khepin_github_auth.client_secret%

</argument>

</call>

</service>

</services>

</container>

The preceding code defines the client_id and client_secret parameters as well as three others for our implementation classes. It is usually a good practice to define these classnames as parameters. This will allow users to replace your implementation with another one if they need to later on. Those classes are defined with a value, so they don't need to be configured by default. The only parameters that are absolutely necessary are client_id and client_secret.

To load and validate the configuration of your bundle, you need to perform the following three steps:

1. Define the configuration format.

2. Load your XML configuration.

3. Merge it with the user-defined configuration.

When you create a bundle through the Symfony generate command, you will usually have a DependencyInjection folder in your bundle. This folder is here exactly for our purpose. It should contain the following two files:

· Configuration.php: This is the file where you define the structure of your configuration

· Extension.php: This is the file where you map the bundle and user-defined configuration together

The Configuration.php file contains the following lines of code:

// Configuration.php

class Configuration implements ConfigurationInterface

{

public function getConfigTreeBuilder()

{

$treeBuilder = new TreeBuilder();

$rootNode = $treeBuilder->root('khepin_github_auth');

$rootNode

->children()

->scalarNode('client_id')

->isRequired()->cannotBeEmpty()->end()

->scalarNode('client_secret')

->isRequired()->cannotBeEmpty()->end()

->scalarNode('authentication_provider_class')->end()

->scalarNode('user_provider_class')->end()

->scalarNode('authentication_listener_class')->end()

->end();

return $treeBuilder;

}

}

We have defined client_id and client_secret as two mandatory parameters for our configuration. We have also declared that our entire specific configuration should be under the khepin_github_auth key. This configuration class defines a specific tree structure that your configuration should stick to. This definition can get much more complex than the current one if, for example, you create multiple configurations of an object. If we wanted to configure multiple entity managers in Doctrine, it would require an array node instead of a scalar one. A simplified version of the code looks as follows:

$node = $treeBuilder->root('entity_managers');

$node

->requiresAtLeastOneElement()

->useAttributeAsKey('name')

->prototype('array')

->addDefaultsIfNotSet()

->children()

->scalarNode('connection')->end()

->scalarNode('class_metadata_factory_name')

->defaultValue('xxx')->end()

->scalarNode('default_repository_class')

->defaultValue('xxx')->end()

->scalarNode('auto_mapping')

->defaultFalse()->end()

->scalarNode('naming_strategy')

->defaultValue('xxx')->end()

->scalarNode('entity_listener_resolver')

->defaultNull()->end()

->scalarNode('repository_factory')

->defaultNull()->end()

->end()

->end()

;

The actual version in DoctrineBundle is a lot longer than this one, but this gives an idea of what is possible. Explaining all the details of what is possible through this configuration file would take a chapter of its own, and it might not be a very interesting one to read. It is possible to set information and examples for each node, validate their type and value, and so on. If you need something more advanced than the simple example here, for the bundle you are building, the best way to learn is to check the core Symfony bundles. They often allow some deep customization and, therefore, have pretty advanced configuration classes.

With this configuration class defined, we know that the configuration we get from the user is formatted properly and can be loaded by our extension class as follows:

class KhepinGithubAuthExtension extends Extension

{

private $namespace = 'khepin_github_auth';

public function load(array $configs, ContainerBuilder $container)

{

$configuration = new Configuration();

$config = $this->processConfiguration(

$configuration,

$configs

);

$loader = new Loader\XmlFileLoader(

$container,

new FileLocator(__DIR__.'/../Resources/config')

);

$loader->load('services.xml');

$this->setParameters(

$container,

$config, $this->namespace

);

}

public function setParameters($container, $config, $ns)

{

foreach ($config as $key => $value) {

$container->setParameter(

$ns . '.' . $key,

$value

);

}

}

}

Most of this file would actually be generated for you. An interesting method is setParameters, which we have defined as a helper method. It takes the parameters in the user config, prefixes them with our configuration namespace, and sets the parameter's value as acontainer parameter. There is no official convention and nothing is enforced by Symfony regarding how you name your parameters, so this notion of namespace with all our parameters prefixed by khepin_github_auth is just for convenience. However, it is not required in any way. Now, all our parameters are correctly set from app/config.yml, which lets the users of our bundle use it in a very simple way.

Note

In a DEV environment, Symfony checks for file changes to see if it needs to reload and revalidate the configuration. This has a high performance cost, so it is not enabled in a PROD environment, where the configuration will be parsed once and cached for later use.

Getting ready to share

With the changes made to the bundle earlier, your bundle is technically ready to be shared between various projects. However, what's left to do? It all depends on your goals, but if you went through all the trouble to create a reusable bundle, maybe even an open source one for all the world to use, then you don't want your efforts to be vain, and you hope that many people will start using your bundle. To improve the adoption and usefulness of your bundles, here's what you should always do.

Research

KNP Labs, a very active company in the Symfony community, created a website (http://knpbundles.com) that lists many Symfony bundles and gives them a score based on popularity, recommendations, activity, testing status, and so on.

A simple search on this website will show us at least two existing bundles for performing authentication through GitHub. It is possible that you have a specific need that is not addressed by these bundles, but in that case, you would do the Symfony community a better service by contacting the author of one of these bundles and trying to improve their work together. One bundle with two authors that fits more (still related) use cases is better and more useful in general than two bundles with a 90 percent functionality overlap and 10 percent specificity.

Documentation

So, your bundle is now available on the Web. It has been indexed on knpbundles as well and people can start using it. There are two kinds of bundles that your fellow developers enjoy or agree to use: the ones that are done so well and have such a clear API that they don't require any documentation to be used (let's settle for very little documentation) and the ones with a clear and extensive documentation. In our case, you can simply add a README file to the bundle, mentioning what it does (user authentication through GitHub), what it needs (FOSUserBundle is a prerequisite), how to install it, and how to configure it.

If your bundle becomes much bigger, think about setting up a small web page for a clearer documentation. The GitHub pages can be very useful here.

Testing

Many people will refuse (with reason) to use a bundle that is not properly tested. There are services (such as Travic.CI) that will let you run the test suite on every single commit you make to your bundle. They will provide you with a little badge to include in your documentation, which will tell the world whether your tests are currently successful or not.

When you are testing a bundle independently of the framework, you don't benefit as much from all the configuration and setup that Symfony does for you. If you have doubts on how you should write your tests or configure a specific service for your tests, it's always a good idea to learn from other bundles that deal with similar problems and gain knowledge from the way they do things.

Let's add some testing to our bundle. First, we make use of a composer to define what libraries we will be using for testing as well as how to autoload our bundle classes. This is done through the autoload, target-dir and require-dev sections of composer.json. The reference to the full composer.json file can be found in the following Distribution and licensing section.

In the Tests folder, we create the following two files:

· phpunit.xml: This file configures phpunit

· bootstrap.php: This file will hold any bootstrapping code that you might need before running your tests, such as configuring a Doctrine connection and mappings, wiring up complex services, and so on

The most basic phpunit configuration will be as follows:

<?xml version="1.0" encoding="UTF-8"?>

<phpunit bootstrap="bootstrap.php">

<testsuites>

<testsuite name="Github Authentication">

<directory suffix=".php">./</directory>

</testsuite>

</testsuites>

</phpunit>

This defines one test suite and tells phpunit to execute bootstrap.php before running any tests.

Note

Remember that phpunit is only one of the possible options for unit testing your bundle. This is the one we use in this book as it is the default one in Symfony, but now, more and more bundles have their tests using different tools such as Atoum (http://atoum.org) or phpspec (http://www.phpspec.net/). For example, the following snippet makes use of Mockery (https://github.com/padraic/mockery) as a replacement for the mocks of phpunit.

Once we have set up our configuration, it is possible to add the first test as follows:

use Khepin\GithubAuthBundle\Security\Github

\AuthenticationProvider;

use \Mockery as m;

class AuthenticationProviderTest extends \PHPUnit_Framework_TestCase

{

public function testAuthenticatesToken()

{

$user = m::mock(['getName' => 'Molly',

'getRoles' => ['ROLE_ADMIN']]);

$user_provider = m::mock(['loadOrCreateUser' => $user]);

$unauthenticated_token = m::mock(

'Khepin\GithubAuthBundle\Security\Github\GithubUserToken',

['getCredentials' => 'molly@example.com']);

$auth_provider = new AuthenticationProvider(

$user_provider);

$token = $auth_provider

->authenticate($unauthenticated_token);

$this->assertTrue($token->isAuthenticated());

$this->assertEquals($token->getUser()->getName(),'Molly');

}

}

Distribution and licensing

Symfony makes heavy use of composer (http://www.getcomposer.org) to manage dependencies, so the best way to get others to use your newly created bundle is to make it available through composer. To do so, we add a simple composer.json file to our bundle as follows:

{

"name": "khepin/github-auth-bundle",

"type": "symfony-bundle",

"description": "Let your user authenticate to a Symfony2 app through their github account",

"keywords": ["authentication, symfony, bundle, github"],

"homepage": "http://xxxx.com",

"license": "MIT",

"authors": [

{

"name": "Machete",

"homepage": "http://en.wikipedia.org/wiki/Machete_(film)"

}

],

"minimum-stability": "dev",

"require": {

"php": ">=5.3.2",

"friendsofofsymfony/user-bundle": "~1.3"

},

{

"mockery/mockery": "*"

},

{

"autoload": {"psr-0": {"Khepin\\GithubAuthBundle": ""}}

},

{

"target-dir": "Khepin/GithubAuthBundle"

}

}

Once this is in place, you can register your package on http://packagist.org, and it will be available for download through composer.

Here, we included the MIT license. There are many existing open source licenses, and if you decide to open source your bundle, you should pick one (or know what it means when you don't). The http://choosealicense.com/ website can help you decide which license is right for you. Symfony itself is MIT licensed, and this is a popular choice for many Symfony bundles.

Is it just a bundle?

A Symfony bundle is meant to be used only within Symfony. By making your code available as a bundle, you limit it to the people using the Symfony framework. The audience for whom you have created the bundle might actually be larger than that within the PHP world. In Chapter 2, Commands and Templates, we introduced the idea that commands in Symfony should only be a very thin wrapper around a service. Well, your bundle should also be a very thin wrapper when possible.

The example we followed in this chapter is for GitHub authentication. It is well suited as it is being fully packaged as a bundle due to the following reasons:

· It only deals with authentication in the Symfony way. Other frameworks or PHP without any framework will deal with authentication differently.

· There is very little logic that is not specific to Symfony. The only part where we do things not completely for Symfony is when we call the GitHub API, but it's contained within just 10 lines of code.

In many cases, your bundle will do more. Maybe, instead of just dealing with the authentication as we did here, you could add a full integration of GitHub. This would mean that based on a user, you can browse their repositories, notifications, latest comments, and so on. If you provide this through a bundle, you have most likely developed a complete API client. This will be very valuable for use outside of Symfony and should then be extracted to a separate library. Your bundle will then exist only to bridge the API client and the framework, provide authentication, declare the appropriate services, and so on.

There is no strict rule that suggests when something should or should not be in a bundle, but asking yourself the question whether some functionality could be extracted for reuse outside of Symfony will lead you on the right way!

Summary

With what we saw in all the previous chapters, you know how to craft Symfony extensions that will make your work easy to reuse within your project.

With this final chapter, you learned how to share it between projects, people, and teams. The technical part of creating a bundle that can be shared is relatively easy. Usually, your code will already be structured inside a bundle, and setting up the configuration and the extension is all you will have to worry about.

It is important to also take time to carefully prepare about the non-technical aspects of sharing a Symfony bundle such as documentation, licensing, and testing. This will greatly help your contributions to be noticed and spread among the community.