Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

[pt_BR][jobeet][day7] Include english version

  • Loading branch information...
commit c0222edd091c7ec542fdc330bebbfccd166f45eb 1 parent fc3f639
@rogeriopradoj rogeriopradoj authored
Showing with 619 additions and 0 deletions.
  1. +619 −0 jobeet/pt_BR/07.markdown
View
619 jobeet/pt_BR/07.markdown
@@ -0,0 +1,619 @@
+Day 7: Playing with the Category Page
+======================================
+
+Yesterday, you expanded your knowledge of symfony in a lot of different areas:
+querying with ##ORM##, fixtures, routing, debugging, and custom configuration.
+And we finished with a little challenge to start today.
+
+We hope you worked on the Jobeet category page as today will then be much more
+valuable for you.
+
+Ready? Let's talk about a possible implementation.
+
+The Category Route
+------------------
+
+First, we need to add a route to define a pretty URL for the category page. Add
+it at the beginning of the routing file:
+
+ [yml]
+ # apps/frontend/config/routing.yml
+ category:
+ url: /category/:slug
+ class: sfPropelRoute
+ param: { module: category, action: show }
+ options: { model: JobeetCategory, type: object }
+
+>**TIP**
+>Whenever you start implementing a new feature, it is a good practice to first
+>think about the ~URL~ and create the associated ~route|Route~. And it is
+>mandatory if you removed the default routing rules.
+
+A route can use any column from its related object as a parameter. It can also
+use any other value if there is a related accessor defined in the object class.
+Because the `slug` parameter has no corresponding column in the `category`
+table, we need to add a virtual accessor in `JobeetCategory` to make the route
+works:
+
+ [php]
+<propel>
+ // lib/model/JobeetCategory.php
+</propel>
+<doctrine>
+ // lib/model/doctrine/JobeetCategory.class.php
+</doctrine>
+ public function getSlug()
+ {
+ return Jobeet::slugify($this->getName());
+ }
+
+The Category Link
+-----------------
+
+Now, edit the `indexSuccess.php` template of the `job` module to add the link to
+the category page:
+
+ [php]
+ <!-- some HTML code -->
+
+ <h1>
+ <?php echo link_to($category, 'category', $category) ?>
+ </h1>
+
+ <!-- some HTML code -->
+
+ </table>
+
+ <?php if (($count = $category->countActiveJobs() -
+ ➥ sfConfig::get('app_max_jobs_on_homepage')) > 0): ?>
+ <div class="more_jobs">
+ and <?php echo link_to($count, 'category', $category) ?>
+ more...
+ </div>
+ <?php endif; ?>
+ </div>
+ <?php endforeach; ?>
+ </div>
+
+We only add the link if there are more than 10 jobs to display for the current
+category. The link contains the number of jobs not displayed. For this template
+to work, we need to add the `countActiveJobs()` method to `JobeetCategory`:
+
+<propel>
+ [php]
+ // lib/model/JobeetCategory.php
+ public function countActiveJobs()
+ {
+ $criteria = new Criteria();
+ $criteria->add(JobeetJobPeer::CATEGORY_ID, $this->getId());
+
+ return JobeetJobPeer::countActiveJobs($criteria);
+ }
+
+The `countActiveJobs()` method uses a `countActiveJobs()` method that does not
+exist yet in `JobeetJobPeer`. Replace the content of the `JobeetJobPeer.php`
+file with the following code:
+</propel>
+<doctrine>
+ [php]
+ // lib/model/doctrine/JobeetCategory.class.php
+ public function countActiveJobs()
+ {
+ $q = Doctrine_Query::create()
+ ->from('JobeetJob j')
+ ->where('j.category_id = ?', $this->getId());
+
+ return Doctrine_Core::getTable('JobeetJob')->countActiveJobs($q);
+ }
+
+The `countActiveJobs()` method uses a `countActiveJobs()` method that does not
+exist yet in `JobeetJobTable`. Replace the content of the `JobeetJobTable.php`
+file with the following code:
+</doctrine>
+
+<propel>
+ [php]
+ // lib/model/JobeetJobPeer.php
+ class JobeetJobPeer extends BaseJobeetJobPeer
+ {
+ static public function getActiveJobs(Criteria $criteria = null)
+ {
+ return self::doSelect(self::addActiveJobsCriteria($criteria));
+ }
+
+ static public function countActiveJobs(Criteria $criteria = null)
+ {
+ return self::doCount(self::addActiveJobsCriteria($criteria));
+ }
+
+ static public function addActiveJobsCriteria(Criteria $criteria = null)
+ {
+ if (is_null($criteria))
+ {
+ $criteria = new Criteria();
+ }
+
+ $criteria->add(self::EXPIRES_AT, time(), Criteria::GREATER_THAN);
+ $criteria->addDescendingOrderByColumn(self::CREATED_AT);
+
+ return $criteria;
+ }
+
+ static public function doSelectActive(Criteria $criteria)
+ {
+ return self::doSelectOne(self::addActiveJobsCriteria($criteria));
+ }
+ }
+
+As you can see for yourself, we have refactored the whole code of
+`JobeetJobPeer` to introduce a new shared `addActiveJobsCriteria()` method to
+make the code more [~DRY~ (Don't Repeat
+Yourself)](http://en.wikipedia.org/wiki/Don%27t_repeat_yourself).
+</propel>
+<doctrine>
+ [php]
+ // lib/model/doctrine/JobeetJobTable.class.php
+ class JobeetJobTable extends Doctrine_Table
+ {
+ public function retrieveActiveJob(Doctrine_Query $q)
+ {
+ return $this->addActiveJobsQuery($q)->fetchOne();
+ }
+
+ public function getActiveJobs(Doctrine_Query $q = null)
+ {
+ return $this->addActiveJobsQuery($q)->execute();
+ }
+
+ public function countActiveJobs(Doctrine_Query $q = null)
+ {
+ return $this->addActiveJobsQuery($q)->count();
+ }
+
+ public function addActiveJobsQuery(Doctrine_Query $q = null)
+ {
+ if (is_null($q))
+ {
+ $q = Doctrine_Query::create()
+ ->from('JobeetJob j');
+ }
+
+ $alias = $q->getRootAlias();
+
+ $q->andWhere($alias . '.expires_at > ?', date('Y-m-d H:i:s', time()))
+ ->addOrderBy($alias . '.created_at DESC');
+
+ return $q;
+ }
+ }
+
+As you can see for yourself, we have refactored the whole code of
+`JobeetJobTable` to introduce a new shared `addActiveJobsQuery()` method to make
+the code more [~DRY~ (Don't Repeat
+Yourself)](http://en.wikipedia.org/wiki/Don%27t_repeat_yourself).
+</doctrine>
+
+>**TIP**
+>The first time a piece of code is re-used, copying the code may be sufficient.
+>But if you find another use for it, you need to refactor all uses to a shared
+>function or a method, as we have done here.
+
+<propel>
+In the `countActiveJobs()` method, instead of using `doSelect()` and then count
+the number of results, we have used the much faster `doCount()` method.
+</propel>
+<doctrine>
+In the `countActiveJobs()` method, instead of using `execute()` and then count
+the number of results, we have used the much faster `count()` method.
+</doctrine>
+
+We have changed a lot of files, just for this simple feature. But each time we
+have added some code, we have tried to put it in the right layer of the
+application and we have also tried to make the code reusable. In the process, we
+have also refactored some existing code. That's a typical workflow when working
+on a symfony project. In the following screenshot we are showing 5 jobs to keep
+it short, you should see 10 (the `max_jobs_on_homepage` setting):
+
+![Homepage](http://www.symfony-project.org/images/jobeet/1_4/07/homepage.png)
+
+Job Category Module Creation
+----------------------------
+
+It's time to create the `category` ~module|Module~:
+
+ $ php symfony generate:module frontend category
+
+If you have created a module, you have probably used the
+`propel:generate-module`. That's fine but as we won't need 90% of the generated
+code, I have used the `generate:module` which creates an empty module.
+
+>**TIP**
+>Why not add a `category` action to the `job` module? We could, but as the
+>main subject of the category page is a category, it feels more natural to
+>create a dedicated `category` module.
+
+When accessing the category page, the `category` route will have to find the
+category associated with the request `slug` variable. But as the ~slug|Slug~ is
+not stored in the database, and because we cannot deduce the category name from
+the slug, there is no way to find the category associated with the slug.
+
+Update the Database
+-------------------
+
+We need to add a `slug` column for the `category` table:
+
+<propel>
+ [yml]
+ # config/schema.yml
+ propel:
+ jobeet_category:
+ id: ~
+ name: { type: varchar(255), required: true }
+ slug: { type: varchar(255), required: true, index: unique }
+</propel>
+<doctrine>
+This `slug` column can be taken care of by a Doctrine behavior named
+`Sluggable`. We simply need to enable the behavior on our `JobeetCategory` model
+and it will take care of everything for you.
+
+ [yml]
+ # config/doctrine/schema.yml
+ JobeetCategory:
+ actAs:
+ Timestampable: ~
+ Sluggable:
+ fields: [name]
+ columns:
+ name:
+ type: string(255)
+ notnull: true
+
+</doctrine>
+
+Now that `slug` is a real column, you need to remove the `getSlug()` method from
+`JobeetCategory`.
+
+<propel>
+Each time the `category` name changes, we need to compute and change the `slug`
+as well. Let's override the `setName()` method:
+
+ [php]
+ // lib/model/JobeetCategory.php
+ public function setName($name)
+ {
+ parent::setName($name);
+
+ $this->setSlug(Jobeet::slugify($name));
+ }
+</propel>
+<doctrine>
+>**NOTE**
+>The setting of the slug column is taken care of automatically when you save a
+>record. The slug is built using the value of the `name` field and set to the
+>object.
+</doctrine>
+
+Use the `propel:build --all --and-load` task to update the database tables, and
+repopulate the database with our fixtures:
+
+ $ php symfony propel:build --all --and-load --no-confirmation
+
+We have now everything in place to create the `executeShow()` method. Replace
+the content of the `category` actions file with the following code:
+
+ [php]
+ // apps/frontend/modules/category/actions/actions.class.php
+ class categoryActions extends sfActions
+ {
+ public function executeShow(sfWebRequest $request)
+ {
+ $this->category = $this->getRoute()->getObject();
+ }
+ }
+
+>**NOTE**
+>Because we have removed the generated `executeIndex()` method, you can also
+>remove the automatically generated `indexSuccess.php` template
+>(`apps/frontend/modules/category/templates/indexSuccess.php`).
+
+The last step is to create the `showSuccess.php` template:
+
+ [php]
+ // apps/frontend/modules/category/templates/showSuccess.php
+ <?php use_stylesheet('jobs.css') ?>
+
+ <?php slot('title', sprintf('Jobs in the %s category', $category->getName())) ?>
+
+ <div class="category">
+ <div class="feed">
+ <a href="">Feed</a>
+ </div>
+ <h1><?php echo $category ?></h1>
+ </div>
+
+ <table class="jobs">
+ <?php foreach ($category->getActiveJobs() as $i => $job): ?>
+ <tr class="<?php echo fmod($i, 2) ? 'even' : 'odd' ?>">
+ <td class="location">
+ <?php echo $job->getLocation() ?>
+ </td>
+ <td class="position">
+ <?php echo link_to($job->getPosition(), 'job_show_user', $job) ?>
+ </td>
+ <td class="company">
+ <?php echo $job->getCompany() ?>
+ </td>
+ </tr>
+ <?php endforeach; ?>
+ </table>
+
+~Partials|Partial Templates~
+----------------------------
+
+Notice that we have copied and pasted the `<table>` tag that create a list of
+jobs from the job `indexSuccess.php` template. That's bad. Time to learn a new
+trick. When you need to reuse some portion of a template, you need to create a
+**~partial|Partial Templates~**. A partial is a snippet of ~template|Templates~
+code that can be shared among several templates. A partial is just another
+template that starts with an underscore (`_`).
+
+Create the `_list.php` file:
+
+ [php]
+ // apps/frontend/modules/job/templates/_list.php
+ <table class="jobs">
+ <?php foreach ($jobs as $i => $job): ?>
+ <tr class="<?php echo fmod($i, 2) ? 'even' : 'odd' ?>">
+ <td class="location">
+ <?php echo $job->getLocation() ?>
+ </td>
+ <td class="position">
+ <?php echo link_to($job->getPosition(), 'job_show_user', $job) ?>
+ </td>
+ <td class="company">
+ <?php echo $job->getCompany() ?>
+ </td>
+ </tr>
+ <?php endforeach; ?>
+ </table>
+
+You can include a partial by using the ~`include_partial()`~ helper:
+
+ [php]
+ <?php include_partial('job/list', array('jobs' => $jobs)) ?>
+
+The first argument of `include_partial()` is the partial name (made of the
+module name, a `/`, and the partial name without the leading `_`). The second
+argument is an array of variables to pass to the partial.
+
+>**NOTE**
+>Why not use the PHP built-in `include()` method instead of the
+>`include_partial()` helper? The main difference between the two is the
+>built-in cache support of the `include_partial()` helper.
+
+Replace the `<table>` HTML code from both templates with the call to
+`include_partial()`:
+
+ [php]
+ // in apps/frontend/modules/job/templates/indexSuccess.php
+ <?php include_partial('job/list', array('jobs' => $category->getActiveJobs(sfConfig::get('app_max_jobs_on_homepage')))) ?>
+
+ // in apps/frontend/modules/category/templates/showSuccess.php
+ <?php include_partial('job/list', array('jobs' => $category->getActiveJobs())) ?>
+
+List ~Pagination~
+-----------------
+
+From the second day's requirements:
+
+ "The list is paginated with 20 jobs per page."
+
+To paginate a list of ##ORM## objects, symfony provides a dedicated class:
+[`sfPropelPager`](http://www.symfony-project.org/api/1_4/sfPropelPager). In the
+`category` action, instead of passing the job objects to the `showSuccess`
+template, we pass a pager:
+
+ [php]
+ // apps/frontend/modules/category/actions/actions.class.php
+ public function executeShow(sfWebRequest $request)
+ {
+ $this->category = $this->getRoute()->getObject();
+
+ $this->pager = new sfPropelPager(
+ 'JobeetJob',
+ sfConfig::get('app_max_jobs_on_category')
+ );
+<propel>
+ $this->pager->setCriteria($this->category->getActiveJobsCriteria());
+</propel>
+<doctrine>
+ $this->pager->setQuery($this->category->getActiveJobsQuery());
+</doctrine>
+ $this->pager->setPage($request->getParameter('page', 1));
+ $this->pager->init();
+ }
+
+>**TIP**
+>The `sfRequest::getParameter()` method takes a default value as a second
+>argument. In the action above, if the `page` request parameter does not
+>exist, then `getParameter()` will return `1`.
+
+The `sfPropelPager` constructor takes a model class and the maximum number of
+items to return per page. Add the latter value to your configuration file:
+
+ [yml]
+ # apps/frontend/config/app.yml
+ all:
+ active_days: 30
+ max_jobs_on_homepage: 10
+ max_jobs_on_category: 20
+
+<propel>
+The `sfPropelPager::setCriteria()` method takes a `Criteria` object to use when
+selecting the items from the database.
+</propel>
+<doctrine>
+The `sfDoctrinePager::setQuery()` method takes a `Doctrine_Query` object to use
+when selecting items from the database.
+</doctrine>
+
+<propel>
+Add the `getActiveJobsCriteria()` method:
+</propel>
+<doctrine>
+Add the `getActiveJobsQuery()` method:
+</doctrine>
+
+<propel>
+ [php]
+ // lib/model/JobeetCategory.php
+ public function getActiveJobsCriteria()
+ {
+ $criteria = new Criteria();
+ $criteria->add(JobeetJobPeer::CATEGORY_ID, $this->getId());
+
+ return JobeetJobPeer::addActiveJobsCriteria($criteria);
+ }
+</propel>
+<doctrine>
+ [php]
+ // lib/model/doctrine/JobeetCategory.class.php
+ public function getActiveJobsQuery()
+ {
+ $q = Doctrine_Query::create()
+ ->from('JobeetJob j')
+ ->where('j.category_id = ?', $this->getId());
+
+ return Doctrine_Core::getTable('JobeetJob')->addActiveJobsQuery($q);
+ }
+</doctrine>
+
+<propel>
+Now that we have defined the `getActiveJobsCriteria()` method, we can refactor
+other `JobeetCategory` methods to use it:
+</propel>
+<doctrine>
+Now that we have defined the `getActiveJobsQuery()` method, we can refactor
+other `JobeetCategory` methods to use it:
+</doctrine>
+
+<propel>
+ [php]
+ // lib/model/JobeetCategory.php
+ public function getActiveJobs($max = 10)
+ {
+ $criteria = $this->getActiveJobsCriteria();
+ $criteria->setLimit($max);
+
+ return JobeetJobPeer::doSelect($criteria);
+ }
+
+ public function countActiveJobs()
+ {
+ $criteria = $this->getActiveJobsCriteria();
+
+ return JobeetJobPeer::doCount($criteria);
+ }
+</propel>
+<doctrine>
+ [php]
+ // lib/model/doctrine/JobeetCategory.class.php
+ public function getActiveJobs($max = 10)
+ {
+ $q = $this->getActiveJobsQuery()
+ ->limit($max);
+
+ return $q->execute();
+ }
+
+ public function countActiveJobs()
+ {
+ return $this->getActiveJobsQuery()->count();
+ }
+</doctrine>
+
+Finally, let's update the template:
+
+ [php]
+ <!-- apps/frontend/modules/category/templates/showSuccess.php -->
+ <?php use_stylesheet('jobs.css') ?>
+
+ <?php slot('title', sprintf('Jobs in the %s category', $category->getName())) ?>
+
+ <div class="category">
+ <div class="feed">
+ <a href="">Feed</a>
+ </div>
+ <h1><?php echo $category ?></h1>
+ </div>
+
+ <?php include_partial('job/list', array('jobs' => $pager->getResults())) ?>
+
+ <?php if ($pager->haveToPaginate()): ?>
+ <div class="pagination">
+ <a href="<?php echo url_for('category', $category) ?>?page=1">
+ <img src="/images/first.png" alt="First page" title="First page" />
+ </a>
+
+ <a href="<?php echo url_for('category', $category) ?>?page=<?php echo $pager->getPreviousPage() ?>">
+ <img src="/images/previous.png" alt="Previous page" title="Previous page" />
+ </a>
+
+ <?php foreach ($pager->getLinks() as $page): ?>
+ <?php if ($page == $pager->getPage()): ?>
+ <?php echo $page ?>
+ <?php else: ?>
+ <a href="<?php echo url_for('category', $category) ?>?page=<?php echo $page ?>"><?php echo $page ?></a>
+ <?php endif; ?>
+ <?php endforeach; ?>
+
+ <a href="<?php echo url_for('category', $category) ?>?page=<?php echo $pager->getNextPage() ?>">
+ <img src="/images/next.png" alt="Next page" title="Next page" />
+ </a>
+
+ <a href="<?php echo url_for('category', $category) ?>?page=<?php echo $pager->getLastPage() ?>">
+ <img src="/images/last.png" alt="Last page" title="Last page" />
+ </a>
+ </div>
+ <?php endif; ?>
+
+ <div class="pagination_desc">
+ <strong><?php echo count($pager) ?></strong> jobs in this category
+
+ <?php if ($pager->haveToPaginate()): ?>
+ - page <strong><?php echo $pager->getPage() ?>/<?php echo $pager->getLastPage() ?></strong>
+ <?php endif; ?>
+ </div>
+
+Most of this code deals with the links to other pages. Here are the list of
+`sfPropelPager` methods used in this template:
+
+ * `getResults()`: Returns an array of ##ORM## objects for the current page
+ * `getNbResults()`: Returns the total number of results
+ * `haveToPaginate()`: Returns `true` if there is more than one page
+ * `getLinks()`: Returns a list of page links to display
+ * `getPage()`: Returns the current page number
+ * `getPreviousPage()`: Returns the previous page number
+ * `getNextPage()`: Returns the next page number
+ * `getLastPage()`: Returns the last page number
+
+As `sfPropelPager` also implements the `Iterator` and `Countable` interfaces,
+you can use `count()` function to get the number of results instead of the
+`getNbResults()` method.
+
+![Pagination](http://www.symfony-project.org/images/jobeet/1_4/07/pagination.png)
+
+Final Thoughts
+--------------
+
+If you worked on your own implementation in day 6 and feel that you didn't
+learn much here, it means that you are getting used to the symfony philosophy.
+The process to add a new feature to a symfony website is always the same: think
+about the URLs, create some actions, update the model, and write some templates.
+And, if you can apply some good development practices to the mix, you will
+become a symfony master very fast.
+
+Tomorrow will be the start of a new week for Jobeet. To celebrate, we will talk
+about a brand new topic: automated tests.
+
+__ORM__
Please sign in to comment.
Something went wrong with that request. Please try again.