Saving and retrieving data

By the end of this lesson, you’ll have a practical understanding of how to save and retrieve information from the database. Along the way, we’ll be:

  • Using the data objects defined in the last tutorial to create some posts for your site
  • Using some of Doctrines built in functions to quickly retrieve data
  • Displaying the posts on the frontend.
  • Handling dynamic URI parameters
Follow along using the code available from this GIT repository.
Start - setting_up_data_objects
End - saving_and_retrieving_data

Getters and Setters

In the last tutorial, we defined a data class and synchronised it with the DataBase engine. At present, that still doesn't make it usable because all the properties are private and inaccessable to the program.

Rather than writing a whole set of methods yourself or relying on magic methods (which are slower) there is a command line script which will generate a base set of methods for you.

  1. # Covering a whole namespace
  2. php bin/console doctrine:generate:entities AppBundle
  3. # Cover a single data object
  4. php bin/console doctrine:generate:entities AppBundle/Entity/Post

This command will setup the methods with documentation and type enforcement. If you repeatedly run it on the same entity, it acts in a non-destructive manner and will only generate methods for properties that don't yet exist; because of this if you change an existing property, you will either have to:

  • Modify the method yourself
  • Clear away the methods and then regenerate

My personal choice is to have a space column definitions for writing custom code and then a loud comment declaring that anything beneath is auto-generated. Also, if I manually edit one of the auto-generated methods, it gets moved above the comment line.

By doing this, whenever I make changes to the schema, it means I can delete everything below the comment and auto regenerate the methods with minimal fuss.

Saving

With all the underlying code ready, it is now possible to use the database. This is best explained by walking through an example so, let's create a TestController to demonstrate.

  1. <?php
  2. // src/AppBundle/Controller/TestController.php
  3. namespace AppBundle\Controller;
  4.  
  5. use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
  6. use Symfony\Bundle\FrameworkBundle\Controller\Controller;
  7. use Symfony\Component\HttpFoundation\Request;
  8.  
  9. // Return a Symfony simple HTTP response without template
  10. use Symfony\Component\HttpFoundation\Response;
  11. // Include the post data class
  12. use AppBundle\Entity\Post;
  13.  
  14. class TestController extends Controller
  15. {
  16.     /**
  17.      * @Route("/test", name="test")
  18.      */
  19.     public function indexAction()
  20.     {
  21.       // Get the entity manager which will handle the save operation
  22.       $em = $this->getDoctrine()->getManager();
  23.  
  24.       // Pay attention to the slash, make use of global namespace
  25.       $currentTime = new \DateTime();
  26.  
  27.       // Create a post
  28.       $post = new Post();
  29.       $post->setTitle('First Post');
  30.       $post->setCreatedAt($currentTime);
  31.       $post->setSummary('A first post test');
  32.       $post->setContent('<p>lorem ipsum...</p>');
  33.  
  34.       // Inform the entity manager that the post object is for saving
  35.       $em->persist($post);
  36.  
  37.       // Create a second post
  38.       $post = new Post();
  39.       $post->setTitle('Second Post');
  40.       $post->setCreatedAt($currentTime);
  41.       $post->setSummary('A second post test');
  42.       $post->setContent('<p>lorem ipsum</p>');
  43.  
  44.       // Inform the entity manager that the post object is for saving
  45.       $em->persist($post);
  46.  
  47.       // Perform the actual database operation
  48.       $em->flush();
  49.  
  50.       return new Response('Entities have been saved to the DB');
  51.     }
  52. }

As you can see from the example, it is the Doctrine Entity Manager that is responsible for handling Database operations. When changes are made, whether they be new records or changes to existing ones, a new unit of work is created.

A unit of work is simply a bag of operations that are due to be written to the database; Doctrine only makes these changes when the flush command is given.

The two immediate benefits to this approach are that it allows you to queue multiple operations and run them at the same time (improving performance) and. The other is an easy opportunity not to make the changes persist in case an error occurs during code execution.

In this example we are not returning a template but, instead a response object. This is a simple object sets headers with a HTTP status code of 200 (unless stated otherwise as a second parameter) and the content string.

Data Retrieval

When Doctrine returns results, it is in the form of a result object (or an array if hydration is disabled) which has a few extra utility functions but, is ultimately simple to work with.

For writing the queries, Doctrine offers three possibilities:

  • Doctrine Query Language (DQL) - It is similar to writing SQL except you are writing queries for the object model (entities instead of tables) and some small differences in syntax that make queries shorter.
  • Query Builder - An object oriented API for programmatically creating DQL queries where you get a full set of helper methods and the ability to directly write DQL interchangably.
  • Native SQL - Writing raw queries which get converted in to Doctrine result objects if a result set map is present or result sets the same as using PHP MySQL module.

Repositories

For Symfony, regardless of the route for writing queries you choose, you get access to the entities through repositories. Repositories are data siloes which act as utility classes designed to abstract access away from the code.

Without even defining any repositories for your entites, you already have access to them and some of the helper functions available. Getting the repository for our post class is simple. Put the following code in to your BlogController under the indexAction method.

  1. // Select the appropriate entity as a source
  2. $posts = $this->getDoctrine()
  3. ->getRepository('AppBundle:Post')
  4. // Use one of the built in methods
  5. ->findAll();

Symfony was built with Doctrine in mind, there is a controller ($this) method that gets Doctrine and then we are stating which bundle and entity to be used. For the moment, we’re keeping it simple and and using the findAll() method to retrieve all the posts we have saved to the data store.

With the query made, we can quickly check the result from the controller using the dump method provided by Symfony. This is a version of the native PHP var_dump that offers the output in a much friendlier fashion.

  1. dump($posts);
  2. die();

Now go and visit your blog index page and you should see a representation of the posts we made earlier; click the arrows to expand / collapse the information levels.

Using data in a template

Now we know the data is available, we’ll want to incorporate it in to our site. At the point we tell the the controller to render the template, the second parameter as an array is used to inject data in to the page.

  1. return $this->render("blog/index.html.twig", ["posts" => $posts]);

You should have the following so far

  1. <?php
  2. // src/AppBundle/Controller/TestController.php
  3. namespace AppBundle\Controller;
  4.  
  5. use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
  6. use Symfony\Bundle\FrameworkBundle\Controller\Controller;
  7. use Symfony\Component\HttpFoundation\Request;
  8.  
  9. class BlogController extends Controller
  10. {
  11.     /**
  12.      * @Route("/blog", name="blog_index")
  13.      */
  14.     public function indexAction()
  15.     {
  16.       // Select the appropriate entity as a source
  17.       $repository = $this->getDoctrine()
  18.       ->getRepository('AppBundle:Post')
  19.       // Use one of the built in methods
  20.       ->findAll();
  21.  
  22.       return $this->render("blog/index.html.twig", ["posts" => $posts]);
  23.     }
  24. }

With posts available to the template, the next thing to do will be present it. Modifying the blog index made earlier, change the contents to the following

  1. {# app/Resources/views/blog/index.html.twig #}
  2. {% extends 'common.html.twig' %}
  3.  
  4. {% block splash %}<div style="height:10rem;"></div>{% endblock %}
  5.  
  6. {% block body %}
  7. <div class="container">
  8.   <div class="row">
  9.  
  10.     {% for post in posts %}
  11.     <h2>
  12.         <a href="#">{{ post.title }}</a>
  13.     </h2>
  14.     <p class="lead">
  15.         by <a href="index.php">Start Bootstrap</a>
  16.     </p>
  17.     <p><span class="glyphicon glyphicon-time"></span> Posted on {{ post.createdAt | date() }}</p>
  18.     <hr>
  19.     <img class="img-responsive" src="http://placehold.it/900x300" alt="">
  20.     <hr />
  21.     <p>{{ post.summary }}</p>
  22.     <a class="btn btn-primary" href="#">Read More <span class="glyphicon glyphicon-chevron-right"></span></a>
  23.  
  24.     <hr>
  25.     {% endfor %}
  26.   </div>
  27. </div>
  28. {% endblock %}
  • Unlike PHP native foreach loop where the individual entry goes second, in Twig, we put the singular first
  • Displaying variables works simply by referencing the object then property.
  • Filters can be added by using a pipe and then the filter name.
  • The date filter uses the default timezone by default. This can be changed directly in the brackets using the normal PHP date format or setting a default for Twig itself.

With the above changes in place, reload your blog to show your changes. At this point, you should have a simple listing which shows the two posts we made from the test controller earlier. We want to view the full post however so, we need to extend the blog controller and template to support this.

Linking to the Posts

Creating the post page

So far, our controllers have had fixed URLs but we will be using dynamic data so, things will be a little bit different to how we have set up our pages so far.

  1. <?php
  2. // src/AppBundle/Controller/BlogController.php
  3.  
  4. class BlogController extends Controller
  5. {
  6.   /* ... indexAction ... */
  7.  
  8.   /**
  9.    * @Route("/blog/{id}", name="blog_singular")
  10.    */
  11.   public function singularAction($id)
  12.   {
  13.     $post = $this->getDoctrine()
  14.     ->getRepository('AppBundle:Post')
  15.     ->find($id);
  16.  
  17.     // replace this example code with whatever you need
  18.     return $this->render('blog/singular.html.twig', ['post' => $post]);
  19.   }
  20. }
  • In the route, we wrap the dynamic part in curly braces. This means that it will match the URI path so long as there is a value after the blog part.
  • For using a variable in the URI, it is made available as a function parameter by the same identifier.
  • The repository method “find” uses the defined index for the data object and looks for a single entry. In our case, this is the ID and is the equivalent of using findOneById

With the controller setup, let’s create the template for our blog post. Saving the template in the blog views path, I recommend naming it singular.html.twig or something similar. This naming scheme makes file recognition quicker and standardised.

You don’t have to follow this convention but, I’d strongly recommend adopting some common convention as it will make life easier for you and others as the project grows or gets shared. With a small note in some developer documentation about how things are named, you will have saved time and effort in the future; anything that makes a project predictable is good design.

  1. {# app/Resources/views/blog/singular.html.twig #}
  2. {% extends 'common.html.twig'%}
  3.  
  4. {% block splash %}
  5.   <header>
  6.     <div class="container">
  7.       <div class="row">
  8.         <div class="col-sm-12">
  9.           <h1>{{ post.title }}</h1>
  10.           <p><small>{{ post.summary }}</small></p>
  11.         </div>
  12.       </div>
  13.     </div>
  14.   </header>
  15. {% endblock %}
  16.  
  17. {% block body %}
  18.   <div class="container">
  19.     <div class="row">
  20.       <div class="col-sm-12">
  21.         {{ post.content|raw }}
  22.       </div>
  23.     </div>
  24.   </div>
  25. {% endblock %}

Notice the use of the raw filter. Twig will autoescape HTML for you by default as a protection mechanism. Because we trust the source, we are outputting it as-is when using the raw filter.

Modify the listing

In the index template, there is only a single change we need to make at this point.

  1. <a href="{{ path('blog_singular', {id: post.id}) }}">{{ post.title}}</a>

Notice that now we are using a second parameter for the path function. This second parameter, declares the URI parameters we want to use in conjunctions with the blog_singular route. If wanting to use multiple parameters in the URI, seperate them by a comma just like JSON definitions

With all the necessary changes in place, go ahead and reload your blog and try clicking a post link. You now have a working blog!

Exercises

  • Try changing the URI parameter name in just the controller or template, you should get an error. This demonstrates how the two are bound together.

Communication, our greatest tool

Whether you want to connect, have a problem I can help with or simply want to drop a line, I welcome your words!