Since a post on security was requested, I am going to show you how to secure a route prefix in your application. The symfony2 security component is very powerful and complex. This implementation will be simple, but you should be able to easily build on it. For securing a production application I would strongly recommend using the FOSUserBundle which can be found here. This bundle is written by some of the core developers of symfony2 and will most likely become the “sfGuardPlugin” for symfony2. The symfony1 folks will know what I mean. I am going to forego writing tests for this part because I want to get it out there as fast as possible, since people seem to be having trouble with security. I may write some tests later and update this post with them. [Update: I have done this. The post can be found here.]

In this part we are going to require anyone who tries to access a route that begins with “/admin/” to login using a form. To do this we will need to do a few things. First we need to register the SecurityBundle that ships with the symfony2 framework. Lets do that now. Open up the AppKernel.php file located in the app directory. We need to register the SecurityBundle so find the registerBundles method and add the following bundle to bundles array:

  1. new Symfony\Bundle\SecurityBundle\SecurityBundle() 

Now that we have registered the bundle we can start having fun. Before we start to write some code, we need to understand the fundamentals of how the security component in symfony2 operates. The symfony2 security component can at a high level be broken down into three different subcomponents; Users, Authentication and Authorization. The Users subcomponent simply represents the client that is using your application. The Authentication subcomponent tries to ensure that the user is who he claims to be. The Authorization subcomponent decides whether or not the user, once authorized, is allowed to perform certain actions, view certain data, etc.

Now we are ready configure our application security. In our configuration we are going to tell symfony2 that we want to use our Company\BlogBundle\Entity\User entity as our User provider, how to encode user passwords, which part of the application should be secure and what role the user should have to access those parts. Open up the config.yml in the app/config directory and add the following configuration:

  1. ## Security Configuration 
  2. security: 
  3.     encoders: 
  4.         Company\BlogBundle\Entity\User: 
  5.             algorithm: sha512 
  6.             encode-as-base64: true 
  7.             iterations: 10 
  9.     providers: 
  10.         main: 
  11.             entity: { class: BlogBundle:User, property: username } 
  13.     firewalls: 
  14.         main: 
  15.             pattern: /.* 
  16.             form_login: 
  17.                 check_path: /login_check 
  18.                 login_path: /login 
  19.             logout: true 
  20.             security: true 
  21.             anonymous: true 
  23.     access_control: 
  24.         - { path: /admin/.*, role: ROLE_ADMIN } 
  25.         - { path: /.*, role: IS_AUTHENTICATED_ANONYMOUSLY } 

Lets go over each section in our security configuration. First, take a look at the encoders section. In this section, we define how the user passwords will be encoded for the Company\BlogBundle\Entity\User entity. We will be using the MessageDigestPasswordEncoder that comes with the symfony2 framework, but it is certainly possible to write your own. Here we specify that we want to use sha512 as our encoding algorithm, iterated 10 times and to encode as a base64 string. We will explore encoders in more detail when we modify our fixtures in a just a moment. You can read more about encoders here if you want.

Next is the providers section. The providers is where we tell symfony2 how to retrieve users. We use the entity entry to tell symfony2 that we want to use the Doctrine Entity Provider. Other available providers are In-Memory Provider and Chain Provider. You can read about them here. Under the entity entry we have to define the entity class and the username property. The class tells symfony2 what entity to load to represent a user. Here we have specified that we want to use the User class in the BlogBundle. The property entry is the PHP column name for the username and in our entity class it will be username.

Authorization is managed by the Firewall system in symfony2. This system is made up of listeners that listen for the event and then redirect the request based on the credentials of the user if necessary. The firewalls entry defines the routing pattern for which we want the security listeners to listen. As of right now, the recommended way to secure an application is to define a single firewall that listens to all routes and then use the access_control entry to allow or disallow access based on roles. We will see the access_control entry in just a moment. In the firewalls entry we have used the pattern entry to specify that we want the security listeners to listen for every route. The form_login entry specifies that our authentication method is to login via a form. You can read about other methods here. Under the form_login entry we specify the routes for the login_path and the check_path. The form_login entry has many more options which you can read about here.

Lastly, we have the access_control entry. The entries under access_control specify routing patterns and the roles necessary to access them. We have specified that for any route starting with “/admin/” the ROLE_ADMIN role is required for access otherwise the only role needed is the IS_AUTHENTICATED_ANONYMOUSLY. This is a special role provided by symfony2 that every user has.

Now that our security configuration is in place we need to update our entity classes to conform to the interfaces required by the SecurityBundle. We need to modify our User entity to implement the UserInterface interface. We also need to create a Role class that implements the RoleInterface. Then we will need to modify our fixtures to load the new data into our database.
现在我们的安全配置已经到位了,我们需要更新我们的实体类以符合SecurityBundle所要求的接口。我们需要修改我们的User实体类去实现 UserInterface接口。我们还需要创建一个Role类去实现RoleInterface接口。然后我们需要修改我们的fixture,以便将新数据导入数据库。

Create a new file named Role.php in the src/Company/BlogBundle/Entity directory. Here is the full Role class:
在src/Company/BlogBundle/Entity 目录中创建一个名为Role.php的新文件。以下是完整的Role类代码:

  1. namespace Company\BlogBundle\Entity; 
  3. use Symfony\Component\Security\Core\Role\RoleInterface; 
  4. use Doctrine\ORM\Mapping as ORM; 
  6. /**
  7.   * @ORM\Entity
  8.   * @ORM\Table(name="role")
  9.   */ 
  10. class Role implements RoleInterface 
  11.     /**
  12.       * @ORM\Id
  13.       * @ORM\Column(type="integer")
  14.       * @ORM\GeneratedValue(strategy="AUTO")
  15.       *
  16.       * @var integer $id
  17.       */ 
  18.     protected $id
  20.     /**
  21.       * @ORM\Column(type="string", length="255")
  22.       *
  23.       * @var string $name
  24.       */ 
  25.     protected $name
  27.     /**
  28.       * @ORM\Column(type="datetime", name="created_at")
  29.       *
  30.       * @var DateTime $createdAt
  31.       */ 
  32.     protected $createdAt
  34.     /**
  35.       * Gets the id.
  36.       *
  37.       * @return integer The id.
  38.       */ 
  39.     public function getId() 
  40.     { 
  41.         return $this->id; 
  42.     } 
  44.     /**
  45.       * Gets the role name.
  46.       * 
  47.       * @return string The name.
  48.       */ 
  49.     public function getName() 
  50.     { 
  51.         return $this->name; 
  52.     } 
  54.     /**
  55.       * Sets the role name.
  56.       *
  57.       * @param string $value The name.
  58.       */ 
  59.     public function setName($value
  60.     { 
  61.         $this->name = $value
  62.     } 
  64.     /**
  65.       * Gets the DateTime the role was created.
  66.       *
  67.       * @return DateTime A DateTime object.
  68.       */ 
  69.     public function getCreatedAt() 
  70.     { 
  71.         return $this->createdAt; 
  72.     } 
  74.     /**
  75.       * Consturcts a new instance of Role.
  76.       */ 
  77.     public function __construct() 
  78.     { 
  79.         $this->createdAt = new \DateTime(); 
  80.     } 
  82.     /**
  83.       * Implementation of getRole for the RoleInterface.
  84.       *
  85.       * @return string The role.
  86.       */ 
  87.     public function getRole() 
  88.     { 
  89.         return $this->getName(); 
  90.     } 
  91. }

The Role entity is quite simple. There is only one property named name that houses the name of the role. The class also implements the RoleInterface interface by supplying a getRole method, which just returns the name of the role. Now that the Role class has been created, we need to update the User entity. Open up the User.php file in the src/Company/BlogBundle/Entity. Here are the relevant changes to the code:
Role实体类非常简单。只有一个名为name的属性,用以存放角色名。该类还通过提供getRole方法来实现RoleInterface接口,该方法只是返回角色名。现在Role类已经被创建,接下来我们还需要更新User实体类。打开src/Company/BlogBundle/Entity 中的User.php文件。以下是改变的代码:

  1. namespace Company\BlogBundle\Entity; 
  3. use Doctrine\Common\Collections\ArrayCollection; 
  4. use Doctrine\ORM\Mapping as ORM; 
  5. use Symfony\Component\Security\Core\User\UserInterface; 
  7. /** 
  8.  * @ORM\Entity 
  9.  * @ORM\Table(name="user") 
  10.  */ 
  11. class User implements UserInterface 
  12.     // ... 
  14.     /** 
  15.      * @ORM\Column(type="string", length="255") 
  16.      * 
  17.      * @var string username 
  18.      */ 
  19.     protected $username
  21.     /** 
  22.      * @ORM\Column(type="string", length="255") 
  23.      * 
  24.      * @var string password 
  25.      */ 
  26.     protected $password
  28.     /** 
  29.      * @ORM\Column(type="string", length="255") 
  30.      * 
  31.      * @var string salt 
  32.      */ 
  33.     protected $salt
  35.     /** 
  36.      * @ORM\ManyToMany(targetEntity="Role") 
  37.      * @ORM\JoinTable(name="user_role", 
  38.      *     joinColumns={@ORM\JoinColumn(name="user_id", referencedColumnName="id")}, 
  39.      *     inverseJoinColumns={@ORM\JoinColumn(name="role_id", referencedColumnName="id")} 
  40.      * ) 
  41.      * 
  42.      * @var ArrayCollection $userRoles 
  43.      */ 
  44.     protected $userRoles
  46.     // ...  
  48.     /** 
  49.      * Gets the username. 
  50.      * 
  51.      * @return string The username. 
  52.      */ 
  53.     public function getUsername() 
  54.     { 
  55.         return $this->username; 
  56.     } 
  58.     /** 
  59.      * Sets the username. 
  60.      * 
  61.      * @param string $value The username. 
  62.      */ 
  63.     public function setUsername($value
  64.     { 
  65.         $this->username = $value
  66.     } 
  68.     /** 
  69.      * Gets the user password. 
  70.      * 
  71.      * @return string The password. 
  72.      */ 
  73.     public function getPassword() 
  74.     { 
  75.         return $this->password; 
  76.     } 
  78.     /** 
  79.      * Sets the user password. 
  80.      * 
  81.      * @param string $value The password. 
  82.      */ 
  83.     public function setPassword($value
  84.     { 
  85.         $this->password = $value
  86.     } 
  88.     /** 
  89.      * Gets the user salt. 
  90.      * 
  91.      * @return string The salt. 
  92.      */ 
  93.     public function getSalt() 
  94.     { 
  95.         return $this->salt; 
  96.     } 
  98.     /** 
  99.      * Sets the user salt. 
  100.      * 
  101.      * @param string $value The salt. 
  102.      */ 
  103.     public function setSalt($value
  104.     { 
  105.         $this->salt = $value
  106.     } 
  108.     /** 
  109.      * Gets the user roles. 
  110.      * 
  111.      * @return ArrayCollection A Doctrine ArrayCollection 
  112.      */ 
  113.     public function getUserRoles() 
  114.     { 
  115.         return $this->userRoles; 
  116.     } 
  118.     /** 
  119.      * Constructs a new instance of User 
  120.      */ 
  121.     public function __construct() 
  122.     { 
  123.         $this->posts = new ArrayCollection(); 
  124.         $this->userRoles = new ArrayCollection(); 
  125.         $this->createdAt = new \DateTime(); 
  126.     } 
  128.     /** 
  129.      * Erases the user credentials. 
  130.      */ 
  131.     public function eraseCredentials() 
  132.     { 
  133.         $this->setPassword(''); 
  134.     } 
  136.     /** 
  137.      * Gets an array of roles. 
  138.      *  
  139.      * @return array An array of Role objects 
  140.      */ 
  141.     public function getRoles() 
  142.     { 
  143.         return $this->getUserRoles()->toArray(); 
  144.     } 
  146.     /** 
  147.      * Compares this user to another to determine if they are the same. 
  148.      *  
  149.      * @param UserInterface $user The user 
  150.      * @return boolean True if equal, false othwerwise. 
  151.      */ 
  152.     public function equals(UserInterface $user
  153.     { 
  154.         return md5($this->getUsername()) == md5($user->getUsername()); 
  155.     } 
  157.     // ... 
  159. }

If you would rather just download the new User.php than make all of the changes by hand, you can find it here. Nothing special in our new User entity really. We have conformed to the UserInterface interface and also set up a new many-to-many relationship with the Role entity. There is also an AdvancedUserInterface that provides even more functionality, but I am not going to use it in this tutorial. You can read more about it here.

Now that our entities have been updated, we need to update our fixtures so that we can update the data in our database. Open up the FixtureLoader.php file in the src/Company/BlogBundle/DataFixtures/ORM folder. Here are the relevant code changes:

  1. namespace Company\BlogBundle\DataFixtures\ORM; 
  3. use Doctrine\Common\DataFixtures\FixtureInterface; 
  4. use Company\BlogBundle\Entity\Category; 
  5. use Company\BlogBundle\Entity\Post; 
  6. use Company\BlogBundle\Entity\Tag; 
  7. use Company\BlogBundle\Entity\User; 
  8. use Company\BlogBundle\Entity\Role; 
  9. use Symfony\Component\Security\Core\Encoder\MessageDigestPasswordEncoder; 
  11. class FixtureLoader implements FixtureInterface 
  12.     public function load($manager
  13.     { 
  14.         // create the ROLE_ADMIN role 
  15.         $role = new Role(); 
  16.         $role->setName('ROLE_ADMIN'); 
  18.         $manager->persist($role); 
  20.         // create a user 
  21.         $user = new User(); 
  22.         $user->setFirstName('John'); 
  23.         $user->setLastName('Doe'); 
  24.         $user->setEmail(''); 
  25.         $user->setUsername('john.doe'); 
  26.         $user->setSalt(md5(time())); 
  28.         // encode and set the password for the user, 
  29.         // these settings match our config 
  30.         $encoder = new MessageDigestPasswordEncoder('sha512', true, 10); 
  31.         $password = $encoder->encodePassword('admin'$user->getSalt()); 
  32.         $user->setPassword($password); 
  34.         $user->getUserRoles()->add($role); 
  36.         $manager->persist($user); 
  38.         // ... 

A new role with the name ROLE_ADMIN has been created. We have also added a username of john.doe to the user and set a random salt. The only new functionality here is the encoding of the password. Remember back in our security.encoders configuration entry we setup the configuration for a MessageDigestPasswordEncoder. Here we are creating an instance of that class and passing in the parameters as configured in our security.encoders entry. Then we are encoding the password “admin” and setting that encoded password as the password for our user. We have to encode the password using the same settings as we have told the security component of the framework to use or else we wont be able to supply credentials for authentication.

Before we can update our routing and create new controllers and views, we have to run a few commands from the console. Open up a terminal and change to your base project directory. First run the following command to update our database schema to match that of our entities.

  1. php app/console doctrine:schema:update --force 

Next, run the following command to clear out the current contents of the database and reload the data using the new updated fixtures.

  1. php app/console doctrine:data:load

At this point we have our security configuration, entities and data all set up. Now we are going to update our routing and create some new controllers and views to implement the form login. Open up the routing.yml file in the src/Company/BlogBundle/Resources/config directory. Add the following routes to the top of the file.

  1. _security_login: 
  2.     pattern:  /login 
  3.     defaults: { _controller: BlogBundle:Security:login } 
  5. _security_check: 
  6.     pattern:  /login_check 
  8. _security_logout: 
  9.     pattern:  /logout 
  11. admin_home: 
  12.     pattern:  /admin/ 
  13.     defaults: { _controller: BlogBundle:Admin:index } 

We have added some special security routes to our routing file. These routes will be used by us as well as the security component of the symfony2 framework. You might be wondering why we did not define controllers for two of the routes. Remember how I said that the security component worked by listening to the event? As a result of this, the controllers for these routes will never need to be resolved because the security component will intercept the request and handle it. So we can safely use the route names _security_check and _security_logout in our templates. Also, we have added the admin_home route which will act as the home page for the admin section of our application. Since we configured the security.access_control entry of our configuration to only allow a user with the role ROLE_ADMIN to access any route pattern that started with “/admin/”, the admin_home route is the secured route we will be trying to access in just a bit.

You may have also noticed that we are going to be creating two new controllers, the SecurityController and AdminController, so lets do that now. Create a new file named AdminController.php in the src/Company/BlogBundle/Controller directory. Here is the code for the AdminController class.

  1. namespace Company\BlogBundle\Controller; 
  3. use Symfony\Bundle\FrameworkBundle\Controller\Controller; 
  5. class AdminController extends Controller 
  6.     public function indexAction() 
  7.     { 
  8.         return $this->render('BlogBundle:Admin:index.html.twig'); 
  9.     } 

Nothing fancy about the indexAction. It simply renders the index.html.twig template. So lets create that now. Create a new folder in the src/Company/BlogBunde/Resources/views folder named Admin. In this new folder create a file named index.html.twig. Here is the template for the admin home page.

  1. {% extends "BlogBundle::layout.html.twig" %} 
  3. {% block title %} 
  4.     symfony2 Blog Tutorial | Admin | Home 
  5. {% endblock %} 
  7. {% block content %} 
  8.     <h2> 
  9.         Welcome to the Admin Homepage {{ app.user.username }}! 
  10.     </h2> 
  11. {% endblock %} 

The only new thing in this template is the app.user template variable. The app.user variable is how you access the currently logged in user. So, in our application the app.user variable translates to an instance of Company\BlogBundle\Entity\User.

Now that we have our secured page created lets create the SecurityController. Create a new file named SecurityController.php in the src/Company/BlogBundle/Controller folder. Here is the code for the SecurityController class.

  1. namespace Company\BlogBundle\Controller; 
  3. use Symfony\Bundle\FrameworkBundle\Controller\Controller; 
  4. use Symfony\Component\Security\Core\SecurityContext; 
  6. class SecurityController extends Controller 
  7.     public function loginAction() 
  8.     { 
  9.         if ($this->get('request')->attributes->has(SecurityContext::AUTHENTICATION_ERROR)) { 
  10.             $error = $this->get('request')->attributes->get(SecurityContext::AUTHENTICATION_ERROR); 
  11.         } else { 
  12.             $error = $this->get('request')->getSession()->get(SecurityContext::AUTHENTICATION_ERROR); 
  13.         } 
  15.         return $this->render('BlogBundle:Security:login.html.twig'array
  16.             'last_username' => $this->get('request')->getSession()->get(SecurityContext::LAST_USERNAME), 
  17.             'error' => $error 
  18.         )); 
  19.     } 

We created a loginAction method which is what we mapped the _security_login route to. In this action we are first checking for an error and then rendering the login.html.twig template as a response. The error checking may look weird, but basically we are just checking to see if we have been forwarded or redirected to this action. If we have, then we are getting the exception that was generated.

The security component will handle all of the credential validation for us, but we must create the template and supply the necessary parameters. In order for the security component to do the validation for us we must submit a form with _username and _password fields to the _security_check route. Lets create a template that does this. Create a new folder in the src/Company/BlogBunde/Resources/views folder named Security. In this new folder create a file named login.html.twig. Here is the code for the new login template.

  1. {% extends "BlogBundle::layout.html.twig" %} 
  3. {% block title %} 
  4.     symfony2 Blog Tutorial | Login 
  5. {% endblock %} 
  7. {% block content %} 
  8.     {% if error %} 
  9.         <div class="error">{{ error.message }}</div> 
  10.     {% endif %} 
  12.     <form action="{{ path('_security_check') }}" method="POST"> 
  13.         <table> 
  14.             <tr> 
  15.                 <td> 
  16.                     <label for="username">Username:</label> 
  17.                 </td> 
  18.                 <td> 
  19.                     <input type="text" id="username" name="_username" value="{{ last_username }}" /> 
  20.                 </td> 
  21.             </tr> 
  22.             <tr> 
  23.                 <td> 
  24.                     <label for="password">Password:</label> 
  25.                 </td> 
  26.                 <td> 
  27.                     <input type="password" id="password" name="_password" /> 
  28.                 </td> 
  29.             </tr> 
  30.         </table> 
  31.         <input type="submit" name="login" value="submit" /> 
  32.     </form> 
  33. {% endblock %} 

There is nothing that you should not understand in this template. It is simply submitting our form to the _security_check route. When we submit a form to this route the security component of symfony2 will intercept the request and handle the user authentication for us. If the user is authenticated then he will be redirected to the initial destination otherwise he will be redirected back to the login page.

At some point you may want to logout of the application. Lets create a logout link that is only shown when a user has logged in. Open the layout.html.twig file in the src/Company/BlogBundle/Resources/views directory. Here is the relevant code change to that file.

  1. // ... 
  3. {% block body %} 
  4.     <div id="container"> 
  5.         <header class="clearfix"> 
  6.             <h1> 
  7.                 symfony2 Blog Tutorial 
  8.             </h1> 
  9.             <nav> 
  10.                 <ul> 
  11.                     <li> 
  12.                         <a href="{{ path('show_page', { 'page' : 'about' }) }}"> 
  13.                             About 
  14.                         </a> 
  15.                     </li> 
  16.                     {% if is_granted('IS_AUTHENTICATED_FULLY') %} 
  17.                         <li> 
  18.                             <a href="{{ path('_security_logout') }}"> 
  19.                                 Logout 
  20.                             </a> 
  21.                         </li> 
  22.                     {% endif %} 
  23.                 </ul> 
  24.             </nav> 
  25.         </header> 
  27.         // ... 

We have used a new twig method named is_granted to check to see if the current user is a specific role. The role we check is IS_AUTHENTICATED_FULLY. This is a special role that a user will have if they have been authenticated by the symfony2 security component. If the user has this role then we add another item to our navigation which is a link to the _security_logout route that we defined earlier.

We are finally ready to try our new secured page. Before we do I would suggest you clear your cache. There is no clear cache command yet, but you can either manually delete all of the files and folders in the app/cache directory or if you are on linux/mac you can run the command rm -rf app/cache/* from your base project directory. [Update: There is now a clear cache command. It is php app/console cache:clear.] Now that you have cleared your cache navigate to the /admin/ path in your web browser. You should be redirected to the login page which looks similar to the following p_w_picpath:
最后我们准备测试一个我们的新安全页面。在此之前,我建议您清一下您的缓存。虽然没有清缓存的命令,但是您可以手工删除app/cache下的所有文件和文件夹。如果您在linux/mac环境下,您可以在您项目根目录中运行rm –rf app/cache/*命令来清除缓存。【更新:现在已经有了清缓存的命令:php app/console cache:clear】。现在您已经清除了缓存,在您的网页浏览器中访问/admin/。您将被重定向到登录页,如下图所示:

symfony2 Blog Application Tutorial Login Page

symfony2 Blog Application Tutorial Login Page (Click for larger)

Enter john.doe for the username and admin for the password. When you submit these credentials you should then be redirected to the admin home page which looks similar to this p_w_picpath:

symfony2 Blog Application Tutorial Admin Homepage

symfony2 Blog Application Tutorial Admin Homepage (Click for larger)

You should also see the Logout link in the header navigation. If you click the Logout link you should be logged out and then redirected to the home page. Whew! That was a lot of work and a lot of trial and error on my part that you didn’t see! We have successfully secured a route prefix in our application. As I said earlier, I would strongly recommend using the FOSUserBundle in a production application. It has a lot more functionality than I have described in this tutorial. I hope you were able to follow along. If any part needs clarification, just ask. Leave me a comment and let me know what you would like to see next. As of right now, I am planning on exploring the symfony2 container and other internals. Until next time…

Update: I created a test for the admin index action that involves logging in using the security bundle. Unfortunately at this time sessions are not supported in the testing framework, so the test will always fail. I am only posting this so you all can learn more about testing. You can follow the comments to understand what is going on. If you want to you could edit your config_test.yml file to change your security configuration and create an http basic user and supply the credentials with the request. Right now this is really the only way to test secured routes. An example of this can be found in the Testing section of the symfony2 book on the symfony2 website.

  1. namespace Company\BlogBundle\Tests\Controller; 
  3. use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; 
  5. class AdminControllerTest extends WebTestCase 
  6.     public function testIndex() 
  7.     { 
  8.         $client = $this->createClient(); 
  9.         $client->followRedirects(true); 
  11.         // request the index action 
  12.         $crawler = $client->request('GET''/admin/'); 
  14.         $this->assertEquals(200, $client->getResponse()->getStatusCode()); 
  16.         // select the login form 
  17.         $form = $crawler->selectButton('submit')->form(); 
  19.         // submit the form with bad credentials 
  20.         $crawler = $client->submit( 
  21.             $form
  22.             array
  23.                 '_username' => 'john.doe'
  24.                 '_password' => 'wrong_password' 
  25.             ) 
  26.         ); 
  28.         // response should be success 
  29.         $this->assertTrue($client->getResponse()->isSuccessful()); 
  31.         // we should have been redirected back to the login page because 
  32.         // invalid credentials were supplied 
  33.         $this->assertTrue($crawler->filter('title:contains("Login")')->count() > 0); 
  35.         // select the login form 
  36.         $form = $crawler->selectButton('submit')->form(); 
  38.         // submit the form with valid credentials 
  39.         $crawler = $client->submit( 
  40.             $form
  41.             array
  42.                 '_username' => 'john.doe'
  43.                 '_password' => 'admin' 
  44.             ) 
  45.         ); 
  47.         // response should be success 
  48.         $this->assertTrue($client->getResponse()->isSuccessful()); 
  50.         // check the title of the page matches the admin home page 
  51.         $this->assertTrue($crawler->filter('title:contains("Admin | Home")')->count() > 0); 
  53.         // check that the logout link exists 
  54.         $this->assertTrue($crawler->filter('a:contains("Logout")')->count() > 0); 
  55.     } 
  56. }