December 4, 2012

Symfony Day 6 - Planning a Forum App

I have Symfony configured, my database ready and now I'm ready to develop my first real application.

I've chosen a simple forum application that I believe will use what I've learned and introduce me to a few other things like user authentication and ajax interactions.

Here are the starting requirements I've outlined for my app:

  • Users will exist with three different permission levels: guests, users, and admins
    • Guests can browse forum threads
    • Users can browse and post to forum threads
    • Admins can browse, post, and create forum threads
    • Admins can change user permission levels
  • Threads will be listed ordered with the most recently updated first
  • Posts will be listed ordered with the most recent first
  • Threads should have a name, a creator, and should know when they were created
  • Posts should have an author, content, and should know when they were created
  • Threads and Posts can only be deleted by an administrator
  • When a user logs in, note their last access date

With these requirements in mind, I see the need for the following views:
  • Thread list page - the list of all threads
  • Thread view page - the list of all posts in a thread
  • The post view - for viewing a single post
  • The user page - for viewing users of the forum (and editing if an admin)
Here are my mockups:



November 28, 2012

Symfony - Day 5.5 - Working with the Database

Previously, I configured Propel, built my database, and built my models. Now I want to use those models to do some work in the database.

A New Route
I'm going to create three new routes for the various actions I want to perform on my database: select, insert, update, delete.

I'm going to update Company/DemoBundle/Resources/config/routing.yml and add these lines:


company_demo_insert:
    pattern: /db/insert/{name}/{price}/{desc}
    defaults: { _controller: CompanyDemoBundle:Default:insert }
    
company_demo_view:
    pattern: /db/view/{id}
    defaults: { _controller: CompanyDemoBundle:Default:view }

company_demo_change_price:
    pattern: /db/update/{id}/{price}
    defaults: { _controller: CompanyDemoBundle:Default:update }
    
company_demo_delete:
    pattern: /db/delete/{id}
    defaults: { _controller: CompanyDemoBundle:Default:delete }

This sets up four new routes that will let us perform some basic functions on our database.


Accessing the Model
To have access to the model I need to update my DefaultController class to use the Product classes. Additionally, I want to return Response objects so I'm updating my class to this:


use Company\DemoBundle\Model\Product;
use Company\DemoBundle\Model\ProductQuery;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Response;

class DefaultController extends Controller
{
   ...



Persisting an Object
We can use our first route company_demo_insert to create our first record in our database. In our DefaultController we need to create the appropriate controller:


public function insertAction($name, $price, $desc)
{
     $product = new Product();
     $product->setName($name);
     $product->setPrice($price);
     $product->setDescription($desc);
        
     $product->save();
        
     return new Response("Added: " . $product->getId());
}

Fairly straightforward right? We defined the controller as insertAction and we define the three arguments we require: $name, $price, and $desc. From there we simply create a new Product (note this was the name of our table in the schema file) and set the name, price, and description using the methods that were created by Propel. When we're done we need to call the save() method to save the changes to the database.

Now I can navigate to the route in my browser: 

[LOCALHOST PATH]/app_dev.php/db/insert/Product A/9.99/Our first Product

My browser pages simply spits back "Added: 1" which is great. This means that our first record with an id of 1 has been added. If I run it again I would get "Added: 2" since the id would be 2.



Fetching an Object
We can use our second route company_demo_view to get an object out of the database. In our DefaultController we need to create the appropriate controller:


public function viewAction($id)
{
     $product = ProductQuery::create()
                ->findPk($id);
        
     if(!$product)
     {
         throw $this->createNotFoundException('Not found');
     }
        
     return new Response($product->getPrice());
}

Again this should be pretty simple. The ProductQuery class was created by Propel when we created our models. Here we create a query that finds a row by the primary key (the id column) and then we simply output the price of the item in the response. We also include a check to make sure the product was found. If not we use the createNotFoundException method of the parent Controller class to return a 404 error that tells the user the item was not found.

So navigating to the route

[LOCALHOST PATH]/app_dev.php/db/view/1

spits out: "9.99". Awesome. Well not really, but it is a big step.


Updating an Object
Prices tend to change, so we need a way to update the price of an existing item. We've created the company_demo_change_price for just this purpose. Our route requires the id and price so we'll accept these as arguments: (note that the order of the arguments is not important)


public function updateAction($price, $id)
{
        $product = ProductQuery::create()
                ->findPk($id);
        
        if(!$product)
        {
            throw $this->createNotFoundException('Not found');
        }
        
        $product->setPrice($price);
        $product->save();
        
        return new Response('Updated');
}

This is really a combination of the methods we've used already. We find the product like we did in the viewAction and then set the price and save like we did in insertAction. Again we check for the existence of the product before making changes.

Let's try it out. Here I'm going to update the price from $9.99 to $5.99.

[LOCALHOST PATH]/app_dev.php/db/update/1/5.99

And it tells me "Updated". If I view my price now:

[LOCALHOST PATH]/app_dev.php/db/view/1

I get "5.99". Done. :)


Deleting an Object
So we've sold out of our item. We need to remove it from the database. All we need is the id of the item and we can remove it:


public function deleteAction($id)
{
     $product = ProductQuery::create()
                ->findPk($id);
        
     if(!$product)
     {
         throw $this->createNotFoundException('Not found');
     }
        
     $product->delete();
        
     return new Response('Deleted');
}

Everything here should look familiar, except the delete function which is self explanatory. Navigating to the route:

[LOCALHOST PATH]/app_dev.php/db/delete/1

I see: "Deleted". Checking my database I see the record no longer exists and when I navigate to the view route I get a 404 error. 




Symfony - Day 5 - Setting up Propel

In my previous post I indicated that I would be working with Propel. Although Propel is one of the two recommended ORMs for Symfony, it does not come with the standard Symfony install so it needs to be manually configured. Propel includes a guide here that walks you through the installation. Since I'm using composer it's easy to obtain the new Propel vendor bundle by adding the line from Propel's guide in composer.json, deleting composer.lock and running the composer install again.

Now we need to configure Propel, create our models and build our database.

Configuring Propel
The Symfony documentation does a pretty good job of walking through this process. However, I noticed that it used "mysql" for the database driver, which produced an error. I had to change this to "pdo_mysql". Here is what my parameters file looks like:

#app/config/parameters.yml
-----
parameters:
    database_driver:   pdo_mysql
    database_host:     localhost
    database_port:     ~
    database_name:     symfony2
    database_user:     root
    database_password: ~


propel:
  dbal:
    driver:           %database_driver%
    user:             %database_user%
    password:     %database_password%
    dsn:              %database_driver%:host=%database_host%;
dbname=%database_name%;charset=UTF8
    options:          {}
    attributes:       {}

-----

Creating the Database
Now that propel knows about the database, it can create it for us:

php app/console propel:database:create

Creating the Models
In order to work with our database, we want to use objects that map to our database. This allows the code to continue working regardless of the database driver. These objects are called models. You could spend a lot of time building these models to talk to each table, but Propel can do this for us if we tell it how are tables are (or will be) structured. Propel can also create our tables for us (more on that in a minute).

To define out table structure we need to create schema.xml in Resources/config under our CompanyBundle. Using the Symfony example we create our file:


<?xml version="1.0" encoding="UTF-8"?>
<database name="default" namespace="Company\DemoBundle\Model" defaultIdMethod="native">
    <table name="product">
        <column name="id" type="integer" required="true" primaryKey="true" autoIncrement="true" />
        <column name="name" type="varchar" primaryString="true" size="100" />
        <column name="price" type="decimal"/>
        <column name="description" type="longvarchar" />
    </table>
</database>

To build our model, we simply need to run one command:

php app/console propel:model:build

Great. Now if we had tables and data we could perform operations. But I haven't created tables yet. Propel can do this for us as well:

php app/console propel:sql:build
php app/console propel:sql:insert --force

This creates our tables and commits that changes. Now we can being using our models.

Additional Modifications
In the Symfony tutorial, they use the price column to store decimal values like "19.99". However, with the xml file above 19.99 will be saved as 20 because I haven't specified a scale for the decimal and the default is 0. I update the column element in the XML to this:

<column name="price" type="decimal" size="10" scale="2"/>

I'll need to update my tables. Since I haven't added any data I can simple drop the database and rebuild it using the three commands before:

php app/console propel:model:build

php app/console propel:sql:build
php app/console propel:sql:insert --force

Note: I tried adding just the "scale" attribute to the XML element, but it didn't have any affect until I added the "size" attribute.

Symfony ORMs

Symfony easily integrates with Doctrine and Propel, two great ORM solutions. I did some quick research on these two and chose Propel. The debate to determine which is better resembles every other tech debate. There are lovers and haters. Some irrational, and some with valid arguments. I'm not going to spend three weeks analyzing which is better. In my short time researching I saw more clean examples of Propel so it wins for now. I'm more interested in Symfony at this point, but I plan to come back and do a full analysis of Doctrine versus Propel in the future.

November 21, 2012

Symfony - Day 4 - Hello {name}


Compared to Zend, Symfony requires me to do a little more work to setup my first page. But those extra steps also give me a lot more power right off the bat. Today I'm just going to get a basic page setup that accepts the "name" parameter in the url and echoes it back out in the form "Hello {name}!". Simple, right? I hope.

I don't want to recreate the Symfony documentation, but I need to outline my steps. In Zend, the default framework install always renders an HTML view. In Symfony, there is a way to use special annotations to accomplish this, but it is definitely optional. The pro here is that I can specify exactly what Symfony should return with a little more clarity than in Zend. The con is that I have to be sure to manually return something every time.

First, we need to define the route that we'll use for this page. When the front controller receives the request is looks at known routes and finds the first match. If it fails to find a match the user will see an error.

To create the new route I open up:
APP_PATH/src/Company/DemoBundle/Resources/routing.yml.

I want to start from scratch so I delete the existing contents of the file (Symfony created this when the bundle was generated). I enter the following route information:

company_demo_default_hello:
    pattern: /hello/{name}
    default: {_controller: CompanyDemoBundle:Default:hello } 


The first line is the name of the route. I want to keep all my routes organized, so I use the pattern {COMPANY}_{BUNDLE}_{CLASS}_{CONTROLLER}. This may be overkill, but for now it works.

The second line is the pattern that we want to match. The brackets around "name" indicated that this is a value that symfony should expect. If the value isn't there then the route isn't matched and we'll get an error.

The third line sets the defaults for this route. Here we need to tell symfony which controller to run. The convention follows a familiar pattern: {BUNDLE}_{CLASS}_{CONTROLLER}.

Done. Our route will now point users to the hello controller, which I'm going to create now.

I open up ".../DemoBundle/Controller/Default". I remove the auto-created defaultAction function since I removed the route as well. From here I need to tell Symfony I want to use the Response object in my code. The Response object is the primary way of getting information back to the user. There are variants such as the RedirectResponse object, but all controllers will return a Response object of some kind. I add the following line just above my class declaration:

use Symfony\Component\HttpFoundation\Response;

and add my new action in the class:

public function helloAction()
{
    return new Response('Hello!');


Let's test. Running localhost/APP_PATH/web/app_dev.php/hello/Bob displays "Hello!". Note that right now I haven't connected "Bob" to my action. However I must include "Bob" for the route to match.

Let's add the name now.

I just need to update my helloAction code to expect the name parameter:

public function helloAction($name)
{
    return new Response('Hello ' . $name . '!');


There we go. Now navigating to the page I see "Hello Bob!". Changing the name to "John" gives me "Hello John!". Symfony automatically connects the {name} from the routing.yml file to the $name argument of the helloAction. What's really cool, is that Symfony doesn't care how many arguments or what order I put them in, it will match up my arguments by name so that I have them available in my controller.

Now we need to set a default. If I don't put a name in the URL, I need it to default to "Sam". So I open up my routing.yml one more time and update the defaults parameter:

defaults: { _controller: CompanyDemoBundle:Default:hello, name: "Sam" }

Now when I visit app_dev.php/hello I get "Hello Sam!".

Note that ".../app_dev.php/hello/" will not lead me here. The extra slash does not match the route. I want to look into this some more to better understand. But I can add an extra route with the pattern "/hello/" and direct it to the same controller as a workaround. The other option is using routing annotations,  but I'm gonna save that for another day.

November 18, 2012

Symfony - Day 3 - My First Bundle


Like Zend, an application can be built in the main application's source directory (APP_PATH/src) but for organizational purposes it's best to group code by functionality. Zend calls these 'modules'. Symfony calls these 'bundles'.

I need to create my first bundle. It will house all my test and demo functionality as I experiment in Symfony. It should be as easy as running this one command from my project directory:

php app/console generate:bundle --namespace=Company/DemoBundle --format=yml

As usual I accept all the defaults.

Now when I look into my APP_PATH/src directory I see a new folder with the name 'Company' which contains a folder 'DemoBundle'.

Note: Bundles can be created manually but it requires creating new files and modifying existing configurations.

November 16, 2012

Tip: Installing PEAR Packages


During the course of my PEAR testing, I attempted to uninstall phpDocumentor and reinstall it. This caused me about 15 minutes of grief as PEAR kept telling me "No releases available for package 'pear.phpdoc.org/phpDocumentor'". I finally found the command I needed: 

pear clear-cache

That did the trick. I was then able to reinstall phpDocumentor without any problems.

November 15, 2012

Installing PhpDocumentor (and PEAR)


I need to learn to document better. I've always done a halfway decent job, but my method is slow and somewhat redundant. Time to pull in a good tool to help me out.
I'm working on a local wamp host. PhpDocumentor says I should install using PEAR. Well that's great - I don't have PEAR installed.

I found this video and followed most of the instructions.
Note that his first execution of go-pear uses the incorrect extension (.php instead of .phar)
Here were my steps:
  1. Download go-pear from http://pear.php.net/go-pear.phar and store it in my php directory
  2. Open my command prompt, navigate to my php directory and run

    php go-pear.phar
  3. At each prompt, I simply press enter, accepting the defaults
With pear installed, I now just need to install phpDocumentor in two simple steps:

pear channel-discover pear.phpdoc.org
pear install phpdoc/phpDocumentor-alpha

I'm hoping that's all I need to do. Let's give it a test run.

First Attempt

With no expectation that this will actually work the first time, I run this:

phpdoc run -d my\code\path -t \my\target\path

Bummer! Well it was wishful thinking anyway. Here's what I see:

[phpDocumentor\Plugin\Core\Exception]
The XSL writer was unable to find your XSLTProcessor; please check if you have installed the PHP XSL extension

I'm not familiar with this extension, although I'm fairly certain I've seen it mentioned enough that I should become acquainted with it. A quick Google search nets me this Stack Overflow post where a DocBlox user has encountered the same problem. Apparently wamp did not enable the xsl extension for php. Opening up php.ini I search for "xsl" and find this:

;extension = php_xsl.dll

I remove the semicolon to uncomment this line and save. Out of habit, I restart all my wamp services as well.

Second Attempt

I run the same commmand again:

phpdoc run -d my\code\path -t \my\target\path

I'm pleasantly surprised this time. It did throw some questions about GraphViz being installed, but it seems to have worked. Since my target path was in web directory I can now browse to the compiled documentation on my localhost. Pretty spiffy. And not too bad of a process.

Update: I've since researched GraphViz and it provides a neat way to visualize your class hierarchy when properly installed. It is completely optional. I personally have not found it useful yet, but plan to keep it around. You can download it here.

Symfony - Day 2 - Installing Symfony


I work on a large campus, so I've been carrying around my Nexus 7 reading the Symfony documentation on my 15-20 minute shuttle rides. It's time to build something.

Unfortunately the installation did not go as smoothly as I had hoped. Symfony relies heavily on Composer as a dependency management library. Wanting to get the full experience, I followed Symfony's installation guide as closely as I could. This proved to be more difficult that anticipated even though Composer installed perfectly. On the Symfony install page, I see that I need to run this:

composer create-project symfony/framework-standard-edition my/install/path 2.1.3

Note: I did not use "composer.phar" as the Symfony page indicates since I performed the Composer global install

This produced the following error:

[Composer\Downloader\TransportException]  The "http://nodeload.github.com/symfony/symfony-standard/zipball/v2.1.3" file could not be downloaded (HTTP/1.1 404 Not Found)

After much digging, and a couple of botched manual installations I found a reference to the "--prefer-source" tag. Running the following composer line gave me exactly what I needed:

php composer create-project symfony/framework-standard-edition my/install/path 2.1.3 --prefer-source

Essentially this clones directly from github rather than downloading the zipball. Regardless, I now have a clean copy of Symfony with vendors installed and a git repository already in place.
At this stage, I don't have a need to get into the folder permission or rewriting aspects as I'm working solely on a xampp localhost. I'll cross that bridge when the time comes.

November 14, 2012

Symfony - Day 1 - Forgetting Zend


A little over a year ago I had my first introduction to the Zend Framework. Starting a new job, my first task was to rewrite an existing web app. The original app had been written years before and had leveraged a lot of Zend framework libraries, but I faced two challenges. First, the original developer was new to both PHP and Zend when he wrote it. And second, I knew almost nothing about Zend. I always like a challenge, but having never touched a PHP framework, or any framework for that matter, I wasn’t exactly sure where to even start. The app did it’s job, but was completely unmanageable, hard to read, and hard to improve. Requirements were stacking up for new features and they just weren’t going to play nice with the existing code. So I spent the first six months reverse engineering the existing app, learning Zend, and deploying a better version. Overall it was a very successful and rewarding experience. Using the full Zend framework allowed me to build quickly and even expand the apps feature list considerably in the process.

I’ve now completed two or three large projects doing things the Zend way, and although I’m pleased with the end result I’m left with a feeling that things can be done better. I read an interesting article the other day about PHP versus Ruby where Derek Sivers compares languages to girlfriends and how we often feel that one language is inferior to another. While certainly there are times when one language is better suited to a task, often times it is our growth as developers that highlight our ignorance and affinity for bad habits in the past. Rather than look on our past self negatively, we blame the language (or girlfriend) we were using at the time. I’m sure that I have not utilized Zend exactly as its designers intended. I’m sure I’ve written code that could be optimized. But I’ve gotten into a rut with the way I build applications in Zend.

It’s time to learn from another framework. I have a new project that is perfectly suited for this experiment. I’ve looked over the various frameworks that are out there and I’ve seen a lot of good things. I narrowed things down to Yii and Symfony and I just felt more at home in the Symfony documentation so I’ll give it a shot. Some may lament that decision for a variety of reasons, but ultimately if this app works and I’ve learned something new then I can call this process a success.

So now to figure out this composer thing…