Before we start
If you already have a jobeet_category_affiliate
table in your database
schema, you can skip this section and go straight to the Previously on
Jobeet
section.
If you have a jobeet_job_affiliate
table, keep reading.
The Jobeet tutorial is made on a day-to-day basis. We really appreciate all the feedback you give us in real-time. Based on some comments from yesterday tutorial, we decided to change the database schema. Instead of having a many-to-many relationship between affiliates and jobs, some of you suggested that it would make more sense to instead have one between affiliates and categories. And we cannot agree more. So, before starting today's tutorial, let's make the change. If won't take too long as most of the things will be automated by the framework.
First replace the jobeet_job_affiliate
table definition with the
jobeet_category_affiliate
one:
// config/schema.yml jobeet_category_affiliate: category_id: { type: integer, foreignTable: jobeet_category, foreignReference: id, required: true, primaryKey: true, onDelete: cascade } affiliate_id: { type: integer, foreignTable: jobeet_affiliate, foreignReference: id, required: true, primaryKey: true, onDelete: cascade }
Rebuild the model classes, re-generate the table creation SQL statements, and insert the data fixtures:
$ php symfony propel:build-all-load
Remove files we don't need anymore:
- lib/filter/base/BaseJobeetJobAffiliateFormFilter.class.php
- lib/filter/JobeetJobAffiliateFormFilter.class.php
- lib/form/base/BaseJobeetJobAffiliateForm.class.php
- lib/form/JobeetJobAffiliateForm.class.php
- lib/model/JobeetJobAffiliate.php
- lib/model/JobeetJobAffiliatePeer.php
- lib/model/map/JobeetJobAffiliateMapBuilder.php
- lib/model/om/BaseJobeetJobAffiliate.php
- lib/model/om/BaseJobeetJobAffiliatePeer.php
Clear the symfony cache to take the changes into account:
$ php symfony cc
We are done.
The SVN tag for day 4 has also been updated to reflect the change.
Previously on Jobeet
Yesterday, we explored how symfony simplifies database management by abstracting the differences between database engines, and by converting the relational elements to nice object oriented classes. We have also played with Propel to describe the database schema, create the tables, and populate the database with some initial data.
Today, we are going to customize the basic job
module we created
yesterday. The job
module already has all the code we need for Jobeet:
- A page to list all jobs
- A page to create a new job
- A page to update an existing job
- A page to delete a job
Although the code is ready to be used as is, we will refactor the templates to match closer to the Jobeet mockups.
The MVC Architecture
If you are used to developing PHP websites without a framework, you probably use the one PHP file per HTML page paradigm. These PHP files probably contain the same kind of structure: initialization and global configuration, business logic related to the requested page, database records fetching, and finally HTML code that builds the page.
You may use a templating engine to separate the logic from the HTML. Perhaps you use a database abstraction layer to separate model interaction from business logic. But most of the time, you end up with a lot of code that is a nightmare to maintain. It was fast to build but over time, it's more and more difficult to make changes, especially because nobody except you understands how it is built and how it works.
As with every problem, there are nice solutions. For web development, the most common solution for organizing your code nowadays is the MVC design pattern. In short, the MVC design pattern defines a way to organize your code according to its nature. This pattern separates the code into three layers:
The Model layer defines the business logic (the database belongs to this layer). You already know that symfony stores all the classes and files related to the Model in the
lib/model
directory.The View is what the user interacts with (a template engine is part of this layer). In symfony, the View layer is mainly made of PHP templates. They are stored in various
templates
directories as we will see later on today.The Controller is a piece of code that calls the Model to get some data that it passes to the View for rendering to the client. When we installed symfony the first day, we saw that all requests are managed by front controllers (
index.php
andfrontend_dev.php
). These front controllers delegate the real work to actions. As we saw yesterday, these actions are logically grouped into modules.
Today, we will use the mockup defined in day 2 to customize the homepage and the job page and make them dynamic. Along the way, we will tweak a lot of things in many different files to demonstrate the symfony directory structure and the way to separate code between layers.
The Layout
First, if you have a closer look at the mockups, you will notice that much of each page looks the same. You already know that code duplication is bad, whether we are talking about HTML or PHP code, so we need to find a way to prevent these common view elements from resulting in code duplication.
One way to solve the problem is to define a header and a footer and include them in each template:
But here the header and the footer files do not contain valid HTML. There must be a better way. Instead of reinventing the wheel, we will use another design pattern to solve this problem: the decorator design pattern. The decorator design pattern resolves the problem the other way around: the template is decorated after the content is rendered by a global template, called a layout in symfony:
The default layout of an application is called layout.php
and can be
found in the apps/frontend/templates/
directory. This directory
contains all the global templates for an application.
Replace the default symfony layout with the following code:
<!-- apps/frontend/templates/layout.php --> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> <head> <title>Jobeet - Your best job board</title> <link rel="shortcut icon" href="/favicon.ico" /> <?php include_javascripts() ?> <?php include_stylesheets() ?> </head> <body> <div id="container"> <div id="header"> <div class="content"> <h1><a href="/job"> <img src="/images/jobeet.gif" alt="Jobeet Job Board" /> </a></h1> <div id="sub_header"> <div class="post"> <h2>Ask for people</h2> <div> <a href="/job/new">Post a Job</a> </div> </div> <div class="search"> <h2>Ask for a job</h2> <form action="" method="get"> <input type="text" name="keywords" id="search_keywords" /> <input type="submit" value="search" /> <div class="help"> Enter some keywords (city, country, position, ...) </div> </form> </div> </div> </div> </div> <div id="content"> <?php if ($sf_user->hasFlash('notice')): ?> <div class="flash_notice"><?php echo $sf_user->getFlash('notice') ?></div> <?php endif; ?> <?php if ($sf_user->hasFlash('error')): ?> <div class="flash_error"><?php echo $sf_user->getFlash('error') ?></div> <?php endif; ?> <div class="content"> <?php echo $sf_content ?> </div> </div> <div id="footer"> <div class="content"> <span class="symfony"> <img src="/images/jobeet-mini.png" /> powered by <a href="http://www.symfony-project.org/"> <img src="/images/symfony.gif" alt="symfony framework" /></a> </span> <ul> <li><a href="">About Jobeet</a></li> <li class="feed"><a href="">Full RSS feed</a></li> <li><a href="">Jobeet API</a></li> <li class="last"><a href="">Affiliates</a></li> </ul> </div> </div> </div> </body> </html>
A symfony template is just a plain PHP file. In the layout template, you will
see calls to PHP functions and references to PHP variables. $sf_content
is
the most interesting variable: it is defined by the framework itself and
contains the HTML generated by the action.
If you browse the job
module (http://jobeet.localhost/frontend_dev.php/job
),
you will see that all actions are now decorated by the layout.
In the layout, we have included a favicon. You can download the Jobeet one and put it under the
web/
directory.
The Stylesheets, Images, and JavaScripts
As we will organize a "best design" contest on the 21st day, we have prepared
a very basic design to use in the meantime:
download the image files
archive and put them into the web/images/
directory;
download the stylesheet files
archive and put them into the web/css/
directory.
By default, the
generate:project
task has created three directories for the project assets:web/images/
for images,web/css/
for stylesheets, andweb/js/
for JavaScripts. This is one of the many conventions defined by symfony, but you can of course store them elsewhere under theweb/
directory.
The astute reader will have noticed that even if the main.css
file is not
mentioned anywhere in the default layout, it is definitely present in the
generated HTML. But not the other ones. How is this possible?
The stylesheet file has been included by the include_stylesheets()
function
call found the layout <head>
tag. The include_stylesheets()
function is
called a helper. A helper is a function, defined by symfony, that can take
parameters and returns HTML code. Most of the time, helpers are time-savers,
they package code snippets frequently used in templates. The
include_stylesheets()
helper generates <link>
tags for stylesheets.
But how does the helper know which stylesheets to include?
The View layer can be configured by editing the view.yml
configuration file
of the application. Here is the default one generated by the generate:app
task:
# apps/frontend/config/view.yml default: http_metas: content-type: text/html metas: #title: symfony project #description: symfony project #keywords: symfony, project #language: en #robots: index, follow stylesheets: [main.css] javascripts: [] has_layout: on layout: layout
The view.yml
file configures the default
settings for all the templates of
the application. For instance, the stylesheets
entry defines an array of
stylesheet files to include for every page of the application (the inclusion
is done by the include_stylesheets()
helper).
In the default
view.yml
configuration file, the referenced file ismain.css
, and not/css/main.css
. As a matter of fact, both definitions are equivalent as symfony prefixes relative paths with/css/
.
If many files are defined, symfony will include them in the same order as the definition:
stylesheets: [main.css, jobs.css, job.css]
You can also change the media
attribute and omit the .css
suffix:
stylesheets: [main.css, jobs.css, job.css, print: { media: print }]
This configuration will be rendered as:
<link rel="stylesheet" type="text/css" media="screen" href="/css/main.css" /> <link rel="stylesheet" type="text/css" media="screen" href="/css/jobs.css" /> <link rel="stylesheet" type="text/css" media="screen" href="/css/job.css" /> <link rel="stylesheet" type="text/css" media="print" href="/css/print.css" />
The
view.yml
configuration file also defines the default layout used by the application. By default, the name islayout
, and so symfony decorates every page with thelayout.php
file. You can also disable the decoration process altogether by switching thehas_layout
entry tofalse
.
It works as is but the jobs.css
file is only needed for the homepage and the
job.css
file is only needed for the job page. The view.yml
configuration
file can be customized on a per-module basis. Change the application
view.yml
file to only contain the main.css
file:
# apps/frontend/config/view.yml stylesheets: [main.css]
To customize the view for the job
module, create a view.yml
file in the
apps/frontend/modules/job/config/
directory:
# apps/frontend/modules/job/config/view.yml indexSuccess: stylesheets: [jobs.css] showSuccess: stylesheets: [job.css]
Under the indexSuccess
and showSuccess
sections (they are the template
names associated with the index
and show
actions, as we will see later on),
you can customize any entry found under the default
section of the
application view.yml
. All specific entries are merged with the application
configuration. You can also define some configuration for all actions of a
module with the special all
section.
Configuration Principles in symfony
For many symfony configuration files, the same setting can be defined at different levels:
- The default configuration is located in the framework
- The global configuration for the project (in
config/
)- The local configuration for an application (in
apps/APP/config/
)- The local configuration restricted to a module (in
apps/APP/modules/MODULE/config/
)At runtime, the configuration system merges all the values from the different files if they exist and caches the result for better performance.
As a rule of thumb, when something is configurable via a configuration file,
the same can be accomplished with PHP code. Instead of creating a view.yml
file for the job
module for instance, you can also use the
use_stylesheet()
helper to include a stylesheet from a template:
<?php use_stylesheet('main.css') ?>
You can also use this helper in the layout to include a stylesheet globally.
Choosing between one method or the other is really a matter of taste. The
view.yml
file provides a way to define things for all actions of a module,
which is not possible in a template, but the configuration is quite static. On
the other hand, using the use_stylesheet()
helper is more flexible and
moreover, everything is in the same place: the stylesheet definition and the
HTML code. For Jobeet, we will use the use_stylesheet()
helper, so you can
remove the view.yml
we have just created and update the job
templates with
the use_stylesheet()
calls.
Symmetrically, the JavaScript configuration is done via the
javascripts
entry of theview.yml
configuration file and theuse_javascript()
helper defines JavaScript files to include for a template.
The Job Homepage
As seen in day 3, the job homepage is generated by the index
action of the
job
module. The index
action is the Controller part of the page and the
associated template, indexSuccess.php
, is the View part:
apps/
frontend/
modules/
job/
actions/
actions.class.php
templates/
indexSuccess.php
The Action
Each action is represented by a method of a class. For the job homepage, the
class is jobActions
(the name of the module suffixed by Actions
) and the
method is executeIndex()
(execute
suffixed by the name of the action).
It retrieves all the jobs from the database:
// apps/frontend/modules/job/actions/actions.class.php class jobActions extends sfActions { public function executeIndex(sfWebRequest $request) { $this->jobeet_job_list = JobeetJobPeer::doSelect(new Criteria()); } // ... }
Let's have a closer look at the code: the executeIndex()
method (the
Controller) calls the Model JobeetJobPeer
to retrieve all the jobs
(new Criteria()
). It returns an array of Job
objects that are assigned to
the jobeet_job_list
object property.
All such object properties are then automatically passed to the template (the View). To pass data from the Controller to the View, just create a new property:
public function executeIndex(sfWebRequest $request) { $this->foo = 'bar'; $this->bar = array('bar', 'baz'); }
This code will make $foo
and $bar
variables accessible in the template.
The Template
By default, the template name associated with an action is deduced by symfony
thanks to a convention (the action named suffix by Success
).
The indexSuccess.php
template generates an HTML table for all the jobs:
<!-- apps/frontend/modules/job/templates/indexSuccess.php --> <h1>Job List</h1> <table> <thead> <tr> <th>Id</th> <th>Category</th> <th>Type</th> <!-- more columns here --> <th>Created at</th> <th>Updated at</th> </tr> </thead> <tbody> <?php foreach ($jobeet_job_list as $jobeet_job): ?> <tr> <td> <a href="<?php echo url_for('job/show?id='.$jobeet_job->getId()) ?>"> <?php echo $jobeet_job->getId() ?> </a> </td> <td><?php echo $jobeet_job->getCategoryId() ?></td> <td><?php echo $jobeet_job->getType() ?></td> <!-- more columns here --> <td><?php echo $jobeet_job->getCreatedAt() ?></td> <td><?php echo $jobeet_job->getUpdatedAt() ?></td> </tr> <?php endforeach; ?> </tbody> </table> <a href="<?php echo url_for('job/new') ?>">New</a>
In the template code, the foreach
iterates through the list of Job
objects
($jobeet_job_list
), and for each job, each column value is output.
Remember, accessing a column value is a simple as calling an accessor method
which name begins with get
and the camelCased column name (for instance the
getCreatedAt()
method for the created_at
column).
Let's clean this up a bit to only display a sub-set of the available columns:
<!-- apps/frontend/modules/job/templates/indexSuccess.php --> <?php use_stylesheet('jobs.css') ?> <div id="jobs"> <table class="jobs"> <?php foreach ($jobeet_job_list as $i => $job): ?> <tr class="<?php echo fmod($i, 2) ? 'even' : 'odd' ?>"> <td><?php echo $job->getLocation() ?></td> <td> <a href="<?php echo url_for('job/show?id='.$job->getId()) ?>"> <?php echo $job->getPosition() ?> </a> </td> <td><?php echo $job->getCompany() ?></td> </tr> <?php endforeach; ?> </table> </div>
The url_for()
function call in this template is a symfony helper that we
will discuss tomorrow.
The Job Page Template
Now let's customize the template of the job page. Open the showSuccess.php
file and replace its content with the following code:
<?php use_stylesheet('job.css') ?> <?php use_helper('Text') ?> <div id="job"> <h1><?php echo $job->getCompany() ?></h1> <h2><?php echo $job->getLocation() ?></h2> <h3> <?php echo $job->getPosition() ?> <small> - <?php echo $job->getType() ?></small> </h3> <?php if ($job->getLogo()): ?> <div class="logo"> <a href="<?php echo $job->getUrl() ?>"> <img src="<?php echo $job->getLogo() ?>" alt="<?php echo $job->getCompany() ?> logo" /> </a> </div> <?php endif; ?> <div class="description"> <?php echo simple_format_text($job->getDescription()) ?> </div> <h4>How to apply?</h4> <p class="how_to_apply"><?php echo $job->getHowToApply() ?></p> <div class="meta"> <small>posted on <?php echo $job->getCreatedAt('m/d/Y') ?></small> </div> <div style="padding: 20px 0"> <a href="<?php echo url_for('job/edit?id='.$job->getId()) ?>">Edit</a> </div> </div>
This template uses the $job
variable passed by the action to display the job
information. As we have renamed the variable passed to the template from
$jobeet_job
to $job
, you need to also make this change in the show
action
(be careful, there are two occurrences of the variable):
// apps/frontend/modules/job/actions/actions.class.php public function executeShow(sfWebRequest $request) { $this->job = JobeetJobPeer::retrieveByPk($request->getParameter('id')); $this->forward404Unless($this->job); }
Notice that some Propel accessors take arguments. As we have defined the
created_at
column as a timestamp, the getCreatedAt()
accessor takes a date
formatting pattern as its first argument:
$job->getCreatedAt('m/d/Y');
The job description uses the
simple_format_text()
helper to format it as HTML, by replacing carriage returns with<br />
for instance. As this helper belongs to theText
helper group, which is not loaded by default, we have loaded it manually by using theuse_helper()
helper.
Slots
Right now, the title of all pages is defined in the <title>
tag of the
layout:
<title>Jobeet - Your best job board</title>
But for the job page, we want to provide more useful information, like the company name and the job position.
In symfony, when a zone of the layout depends on the template to be displayed, you need to define a slot:
Add a slot to the layout to allow the title to be dynamic:
// apps/frontend/templates/layout.php <title><?php include_slot('title') ?></title>
Each slot is defined by a name (title
) and can be displayed by using the
include_slot()
helper. Now, at the beginning of the showSuccess.php
template, use the slot()
helper to define the content of the slot for the
job page:
// apps/frontend/modules/job/templates/showSuccess.php <?php slot('title', sprintf('%s is looking for a %s', $job->getCompany(), $job->getPosition())) ?>
If the title is complex to generate, the slot()
helper can also be used
with a block of code:
// apps/frontend/modules/job/templates/showSuccess.php <?php slot('title') ?> <?php echo sprintf('%s is looking for a %s', $job->getCompany(), $job->getPosition()) ?> <?php end_slot(); ?>
For some pages, like the homepage, we just need a generic title. Instead of repeating the same title over and over again in templates, we can define a default title in the layout:
// apps/frontend/templates/layout.php <title> <?php if (!include_slot('title')): ?> Jobeet - Your best job board <?php endif; ?> </title>
The include_slot()
helper returns true
if the slot has been defined. So,
when you define the title
slot content in a template, it is used; if not,
the default title is used.
We have already seen quite a few helpers beginning with
include_
. These helpers output the HTML and in most cases have aget_
helper counterpart to just return the content:<?php include_slot('title') ?> <?php echo get_slot('title') ?> <?php include_stylesheets() ?> <?php echo get_stylesheets() ?>
The Job Page Action
The job page is generated by the show
action, defined in the executeShow()
method of the job
module:
class jobActions extends sfActions { public function executeShow(sfWebRequest $request) { $this->job = JobeetJobPeer::retrieveByPk($request->getParameter('id')); $this->forward404Unless($this->job); } // ... }
As in the index
action, the JobeetJobPeer
class is used to retrieve a job,
this time by using the retrieveByPk()
method. The parameter of this method
is the unique identifier of a job, its primary key. The next section will
explain why the $request->getParameter('id')
statement returns the job
primary key.
The generated model classes contain a lot of useful methods to interact with the project objects. Take some time to browse the code located in the
lib/om/
directory and discover all the power embedded in these classes.
If the job does not exist in the database, we want to forward the user to a
404 page, which is exactly what the forward404Unless()
method does. It
takes a Boolean as its first argument and, unless it is true, stops the current
flow of execution. As the forward methods stops the execution of the action
right away by throwing a sfError404Exception
, you don't need to return
afterwards.
As for exceptions, the page displayed to the user is different in the prod
environment and in the dev
environment:
Before we deploy the Jobeet website to the production server, you will learn how to customize the default 404 page.
The "forward" Methods Family
The
forward404Unless
call is actually equivalent to:$this->forward404If(!$this->job);which is also equivalent to:
if (!$this->job) { $this->forward404(); }The
forward404()
method itself is just a shortcut for:$this->forward('default', '404');The
forward()
method forwards to another action of the same application; in the previous example, to the404
action of thedefault
module. Thedefault
module is bundled with symfony and provides default actions to render 404, secure, and login pages.
The Request and the Response
When you browse to the /job
or /job/show/id/1
pages in your browser, your
are initiating a round trip with the web server. The browser is sending a
request and the server sends back a response.
We have already seen that symfony encapsulates the request in a sfWebRequest
object (see the executeShow()
method signature). And as symfony is an
Object-Oriented framework, the response is also an object, of class
sfWebResponse
. You can access the response object in an action by calling
$this->getResponse()
.
These objects provide a lot of convenient methods to access information from PHP functions and PHP global variables.
Why does symfony wrap existing PHP functionalities? First, because the symfony methods are more powerful than their PHP counterpart. Then, because when you test an application, it is much more easier to simulate a request or a response object than trying to fiddle around with global variables or work with PHP functions like
header()
which do too much magic behind the scene.
The Request
The sfWebRequest
class wraps the $_SERVER
, $_COOKIE
, $_GET
, $_POST
,
and $_FILES
PHP global arrays:
Method name | PHP equivalent |
---|---|
getMethod() |
$_SERVER['REQUEST_METHOD'] |
getUri() |
$_SERVER['REQUEST_URI'] |
getReferer() |
$_SERVER['HTTP_REFERER'] |
getHost() |
$_SERVER['HTTP_HOST'] |
getLanguages() |
$_SERVER['HTTP_ACCEPT_LANGUAGE'] |
getCharsets() |
$_SERVER['HTTP_ACCEPT_CHARSET'] |
isXmlHttpRequest() |
$_SERVER['X_REQUESTED_WITH'] == 'XMLHttpRequest' |
getHttpHeader() |
$_SERVER |
getCookie() |
$_COOKIE |
isSecure() |
$_SERVER['HTTPS'] |
getFiles() |
$_FILES |
getGetParameter() |
$_GET |
getPostParameter() |
$_POST |
getUrlParameter() |
$_SERVER['PATH_INFO'] |
getRemoteAddress() |
$_SERVER['REMOTE_ADDR'] |
We have already accessed request parameters by using the getParameter()
method. It returns a value from the $_GET
or $_POST
global variable, or
from the PATH_INFO
variable.
If you want to ensure that a request parameter comes from a particular one of
these variables, you need use the getGetParameter()
, getPostParameter()
,
and getUrlParameter()
methods respectively.
When you want to restrict an action for a specific method, for instance when you want to ensure that a form is submitted as a
POST
, you can use theisMethod()
method:$this->forwardUnless($request->isMethod('POST'));
.
The Response
The sfWebResponse
class wraps the header()
and setrawcookie()
PHP
methods:
Method name | PHP equivalent |
---|---|
setCookie() |
setrawcookie() |
setStatusCode() |
header() |
setHttpHeader() |
header() |
setContentType() |
header() |
addVaryHttpHeader() |
header() |
addCacheControlHttpHeader() |
header() |
Of course, the sfWebResponse
class also provides a way to set the content of
the response (setContent()
) and send the response to the browser (send()
).
Earlier today we saw how to manage stylesheets and JavaScripts in both the
view.yml
file and in templates. In the end, both techniques use the response
object addStylesheet()
and addJavascript()
methods.
The
sfAction
,sfRequest
, andsfResponse
classes provide a lot of other useful methods. Don't hesitate to browse the API documentation to learn more about all symfony internal classes.
See you Tomorrow
Today, we have described some design patterns used by symfony. Hopefully the project directory structure now makes more sense. We have played with the templates by manipulating the layout and the template files. We have also made them a bit more dynamic thanks to slots and actions.
Tomorrow, we will learn more about the url_for()
helper we have used today,
and the routing sub-framework associated with it.
Until then, feel free to browse the source of today's tutorial (tag
release_day_04
) at:
http://svn.jobeet.org/tags/release_day_04/
nice meatier than the previous ones
$this->jobeet_job_list = JobeetJobPeer::doSelect(new Criteria());
Why is this piece of code not following symfony coding standarts - "Use camelCase, not underscores, for variable, function and method names"?
P.S. symfony propel:generate-module-for-route admin classifier_groups also generates similar code.
Variables used in the templates should use the underscore syntax according to the symfony coding guidelines. In chapter 2 of the symfony book: "Among the coding standards used in symfony, UpperCamelCase is the standard for class and variable naming. Two exceptions exist: core symfony classes start with sf, which is lowercase, and variables found in templates use the underscore-separated syntax."
@Arne
Thanks for the explanation, this exceptions are not described in the wiki
what font is in the jobeet logo?
I think you left out from showSuccess.php.
When I try to access the SVN I must supply a user name and password. Am I the only one ?
If you get an error after updating the ORM and calling the edit form ("Before we start" from day 4 and after the "symfony propel:build-all-load" command), maybe you have to call also "symfony propel:build-forms" and "symfony propel:build-filters" to correct this.
@Jocelyn Bernier I am also prompted for a login/password...
SVN authentication problem solved
@Fabien Still prompts for username/password.
In the note about deployment and 404 pages it says "SIDEBAR" in the middle of the text. Might be a typo? :)
Great Tutorial!!!
there's a little error in layout.php, the post a new job link's href must be /job/new
Shouldn't be the correct path where "actions.class.php" is located (for renaming "$jobeet_job" to "$job") apps/frontend/modules/job/actions/actions.class.php and not apps/frontend/jobeet/actions/actions.class.php (shown in comment in code snippet)?
http://svn.jobeet.org/tags/release_day_4/ is wrong URL. Correct URL is http://svn.jobeet.org/tags/release_day_04/
@Gianko: THis is not a typo. We will work on URLs in another tutorials as we have to explain how to have different URLs based on the environments with the url_for helper.
@Billy Galinos: fixed
@fivestar: thanks for the hint. fixed now.
Why is the use_stylesheet() helper called in showSucess and indexSuccess? We already included the css files via view.yml earlier in the tutorial.
Great tutorials ;) There's a little typo in the code of layout.php. CSS class "flash_notice" appears twice. I think you meant "flash_notice" and "flash_error" :)
Eric
The "JobeetJobPeer::retrieveByPk" method, used in the "show" action hydrates every property of "job" objects in the collection.
And in the view, only 4 properties of the job object are used... i think it is a bad thing in term of database usage optimization !
Is there a clean and simple way to partially hydrate objects from database with propel in order to insure database performance ?