- Installing OroCRM on Mac OS X
- Oro, Symfony, Logging, and Debugging
- OroCRM Hello World
- OroCRM and the Symfony App Console
- OroCRM Frontend Asset Pipeline
- WebSockets in OroCRM
Last time we walked you through creating a bundle in Symfony and adding it to the OroCRM application. Whenever you’re starting with a new software framework it’s always important to do this sort of step-by-step breakdown so you understand how something works. However, once you have the architecture model in your head, it can tedious, time consuming, and a general pain in the behind to create bundles manually each and every time.
Fortunately, Symfony (and therefore OroCRM/OroBAP) has a solution, and that’s the Symfony console application. This application contains a number of useful commands for developers, including code generation tools. If you’re coming from the Ruby on Rails world this is very similar to the Rails Command line.
In this tutorial you’ll learn how to create a bundle with the Symfony console application. We’ll also, by necessity, touch on Symfony’s cache folder permissions, as well as the annotations configuration syntax.
Important: The following article refers to alpha, pre-release software. While the general concepts should apply to the final version, the specific details are almost certain to change.
Running the Console
Running the Symfony console application is relatively straight forward. Just navigate to your application folder and run the following command
$ cd /path/to/crm-application
$ php app/console
Symfony version 2.1.11 - app/dev/debug
Usage:
[options] command [arguments]
Options:
...
When you run the console
command without any arguments, Symfony will output usage information, including a list of supported commands. If you take a look at the app/console
file, you’ll see it’s just a simple PHP command line script
$ cat app/console
#!/usr/bin/env php
<?php
// if you don't want to setup permissions the proper way, just uncomment the following PHP line
// read http://symfony.com/doc/current/book/installation.html#configuration-and-setup for more information
// umask(0000);
set_time_limit(0);
require_once __DIR__.'/bootstrap.php.cache';
require_once __DIR__.'/AppKernel.php';
//...
This application is built using the Symfony console component. If you’re familiar with the n98-magerun
command line tool you’ll be right at home.
The command we’re interested in is the generate:bundle
command.
$ php app/console list generate
Symfony version 2.1.11 - app/dev/debug
Usage:
[options] command [arguments]
...options ommited...
Available commands for the "generate" namespace:
generate:bundle Generates a bundle
...
However, before we run the generate:bundle
command, we need to have a quick talk about file permissions and the Symfony cache.
Symfony Cache Permissions
Like all modern web application frameworks, Symfony relies heavily on server side caching. This isn’t the “my CSS file won’t update” browser cache, but instead a server side cache where long running operations are performed once, their results stored, and then Symfony uses the stored (or “cached”) results for all future requests (until the cache is cleared).
By default, Symfony stores its cache on the file system. This is a typical setup for development environments, which means you need to be concerned about cache file permissions. This, in turn, is going to lead to a problem with the console
application.
Take a look at the permissions on your cache files
$ ls -l app/cache/
total 0
drwxr-xr-x 61 alanstorm staff 2074 Jul 12 15:53 dev
drwxr-xr-x 50 _www staff 1700 Jul 11 13:13 prod
Your results may be different, but the above illustrates the problem perfectly. In the above example, the production cache was created by a request to the web server. This means it’s owned by the _www
user. My dev cache, on the other hand, was created from running the command line console application. That means it’s owned by the alanstorm
user I was logged in as when I ran the application.
So what’s the problem? By default, a cache folder created by the CLI isn’t writable by the website, and a folder created by the website isn’t writable by the CLI. If Symfony can’t write to the cache, it throws an error. Many of the initial alpha installation problems were related to this. If you take a look at the install.sh
script, you’ll see it’s just a wrapper to some console
calls.
#File: install.sh
#!/bin/sh
php app/console doctrine:database:create
php app/console doctrine:schema:create
php app/console oro:search:create-index
php app/console doctrine:fixture:load --no-debug --no-interaction
php app/console oro:acl:load
php app/console oro:navigation:init
php app/console assets:install web
php app/console assetic:dump
chmod -R 777 app/cache/
chmod -R 777 app/logs/
Unfortunately, there’s no one right solution here. File permissions and web applications have been clashing since the mid-90s, and I’ve yet to see a solution that manages everyone’s needs/expectations perfectly. All of which is a fancy way of saying: Here’s some options, but you’ll need to make up your own mind on the best approach.
First, the recommended Symfony approach is to use your operating system’s ACL features to ensure the web user and your own user have the same access level to the cache
and logs
folders. While this works, it’s not an ideal solution for me. I’m biased towards development environments that are as “unix neutral” as possible. Since I’m using a Mac to develop, that means the ACL command would be a chmod -a
. However, in all likelihood we’ll be deploying to a linux machine which means the setfacl
command is the one we want. That difference in environments is a mental burden I’ll need to carry for the entire project, leaving less room in my brain for more important things.
The second approach, and the one I’m using, is to have PHP’s umask set to 0000
on my local development environment. This ensures files and directories created with PHP will have 777
file permissions, which means they’ll be writable and readable by every user account on my machine. This isn’t an ideal solution, but since I’m not sharing this machine with other users it’s a compromise I can live with.
So how do you set PHP’s umask? You’ll need to edit any file that bootstraps a Symfony environment. In our case, that’s the app/console
file, as well as the web/app_dev.php
file (app_dev_js.php
and app.php
are also candidates, depending on how you’re using your development environments locally).
Open these files, and look for the call to PHP ‘umask’ function
//umask(0000);
Uncomment this line in each file, and then remove any existing cache folders
rm app/cache/dev
rm app/cache/prod
etc...
and you’ll be good to go.
As I said, debates around “the right permissions” to use for web application development are as old as time. Back in the mid-90s I had a sys-admin tell me I couldn’t set my html files to 644
because “the world will be able to see them”. You’ll need to make your own decisions as to which approach is right for you.
Running the Bundle Create Command
With that bit of sys-admin 101 out of the way, we’re ready to start. This time, we’re going to create a bundle named Helloworld2
(distinct from the Helloworld
bundle we created last time). To get started, just run the generate:bundle
command
$ php app/console generate:bundle
Welcome to the Symfony2 bundle generator
Your application code must be written in bundles. This command helps
you generate them easily.
Each bundle is hosted under a namespace (like Acme/Bundle/BlogBundle).
The namespace should begin with a "vendor" name like your company name, your
project name, or your client name, followed by one or more optional category
sub-namespaces, and it should end with the bundle name itself
(which must have Bundle as a suffix).
See http://symfony.com/doc/current/cookbook/bundles/best_practices.html#index-1 for more
details on bundle naming conventions.
Use / instead of \ for the namespace delimiter to avoid any problem.
Bundle namespace:
After running the command you’ll be launched into an interactive command line experience that steps you through bundle creation. We’re going to create a Helloworld2
bundle in the top level Pulsestorm
namespace, so enter Pulsestorm/Bundle/Helloworld2Bundle
as the full namespace.
Bundle namespace: Pulsestorm/Bundle/Helloworld2Bundle
In your code, a bundle is often referenced by its name. It can be the
concatenation of all namespace parts but it's really up to you to come
up with a unique name (a good practice is to start with the vendor name).
Based on the namespace, we suggest PulsestormHelloworld2Bundle.
Bundle name [PulsestormHelloworld2Bundle]:
Next, Symfony is telling us we need to name our bundle. We didn’t do this last time — we just stuck with the default. While it’s possible to have your bundle named differently, in practice this may cause confusion, so let’s just go with the default (PulsestormHelloworld2Bundle
) again
Bundle name [PulsestormHelloworld2Bundle]:
The bundle can be generated anywhere. The suggested default directory uses
the standard conventions.
Target directory [/path/to/crm-application/src]:
The console application is asking us where we want to create our bundle. Again, the default will be fine here. Remember, the whole point of bundles is they group all our code and resources together, meaning they can be easily moved around later.
Target directory [/path/to/crm-application/src]:
Determine the format to use for the generated configuration.
Configuration format (yml, xml, php, or annotation) [annotation]:
Here symfony is asking us what format we’d like our bundle’s configuration to be in. In this case, we’re going to change this from the default (annotation
) to YAML (yml
). We’ll talk more about why below, but for now just sit tight.
Configuration format (yml, xml, php, or annotation) [annotation]: yml
To help you get started faster, the command can generate some
code snippets for you.
Do you want to generate the whole directory structure [no]?
Here Symfony is asking if we’d like a full generation of code, or a minimal one. Let’s say yes
to the whole directory structure.
Do you want to generate the whole directory structure [no]? yes
Summary before generation
You are going to generate a "Pulsestorm\Bundle\Helloworld2Bundle\PulsestormHelloworld2Bundle" bundle
in "/path/to/crm-application/src/" using the "yml" format.
Do you confirm generation [yes]?
Before creating your bundle, the console application will confirm what, exactly, it’s going to do. Review the information, and press enter to continue.
Do you confirm generation [yes]?
Bundle generation
Generating the bundle code: OK
Checking that the bundle is autoloaded: OK
Confirm automatic update of your Kernel [yes]?
Symfony will confirm the bundle’s been generated. At this point, your bundle code has all been generated in the src
directory. Next, Symfony asks if you’d like to automatically update the AppKernel
file. Specifically, this refers to updating the registerBundles
method with a bundle declaration. Hit enter to say yes.
Confirm automatic update of your Kernel [yes]?
Enabling the bundle inside the Kernel: OK
Confirm automatic update of the Routing [yes]? yes
After confirming it was able to update the app/AppKernel.php
file, Symfony will ask if you want to update the bundle’s routing information. Saying yes to this will automatically add a configuration entry to the app/etc/routing.yml
file. This configuration entry will point to your bundle’s routing.yml
file, and enable routing for your bundle. Hit enter to say yes, and bundle creation will be complete.
Importing the bundle routing resource: OK
You can now start using the generated code!
To review, after running the generate:bundle
command we’ll have
- Created the bundle files in
src/Pulsestorm/Bundle/Helloworld2Bundle/
- Updated the
AppKernel
‘sregisterBundles
method with a bundle declaration inapp/AppKernel.php
- Added a
pulsestorm_helloworld2
entry to theapp/etc/routing.yml
file
Congratulations, you just created your first automated bundle. As you can see, this is much less tedious than manually creating individual files. Before we wrap up, let’s get to the caveats we brushed aside earlier.
Updating the AppKernel File
After finishing the bundle:generate
command, try to get a list of commands from the console application. If you’re unlucky, you may run into the following error
$ php app/console list
PHP Parse error: syntax error, unexpected ',', expecting ')' in /path/to/crm-application/app/AppKernel.php on line 61
PHP Stack trace:
PHP 1. {main}() /path/to/crm-application/app/console:0
The console application uses the same PHP bootstrapping process as the main OroCRM application. Unfortunately, while relatively clever, the bundle:generate
‘s AppKernel
updating code isn’t bullet proof. If you take a look at the updated AppKernel
file at the line specified
#File: crm-application/app/AppKernel.php
new OroCRM\Bundle\DashboardBundle\OroCRMDashboardBundle(),
,
new Pulsestorm\Bundle\Helloworld2Bundle\PulsestormHelloworld2Bundle(),
you’ll quickly see the problem. Symfony’s updating code assumes the main $bundle
array does not end in a comma, so it inserts one for you — even if this make the file invalid PHP code. You’ll need to manually remove the extraneous comma.
new OroCRM\Bundle\DashboardBundle\OroCRMDashboardBundle(),
new Pulsestorm\Bundle\Helloworld2Bundle\PulsestormHelloworld2Bundle(
Once you remove the comma, you should be good to go.
Bundle Configuration
One of the bundle creation commands we skipped the defaults on was the following.
Determine the format to use for the generated configuration.
Configuration format (yml, xml, php, or annotation) [annotation]:
This one’s going to require a bit of explanation. You don’t need to understand this fully to continue, so if your head’s starting to spin just let the information soak in for a bit.
In addition to the AppKernel
class, Symfony’s (and therefor Oro’s) architecture philosophy is service oriented dependency injection. Wait! Come back! Dependency injection seems like a fancy complicated term, but at its core it’s pretty simple. Dependency injection allows a programmer to “easily” (i.e. through configuration) swap out the instantiation of one object for another.
In Drupal the concept is called pluggable — the Drupal core team explicitly sets certain classes such that they could swapped out for others via configuration. In Magento, its called class rewrites. With Magento, all models, blocks, and helper objects were made dependency injectable (assuming they use Magento’s factory instantiation methods)
Symfony’s system splits the difference between these two. In Symfony, there’s a special kind of object called a “Service”. This isn’t a web-service like SOAP or REST. A service is simply an object that does something for you, that you (or someone else) might want to change the implementation of.
So, services require configuration, and that’s what Symfony is asking about above. By choosing yml
, we told Symfony to create the following service configuration file
src/Pulsestorm/Bundle/Helloworld2Bundle/Resources/config/services.yml
which in turn is loaded by the special dependency injection class at
src/Pulsestorm/Bundle/Helloworld2Bundle/DependencyInjection/PulsestormHelloworld2Extension.php
We’ll eventually go deeper on how Symfony knows to look for these files. For now just accept it as magic (or dive into the kernel’s handle
method on your own, although reading too far ahead may melt your brain).
So far so good. While conceptually this may be new to you, it’s relatively straight forward. If you were yearning for something more complex, don’t worry, we’re about to ruin that simplicity.
When is Configuration not Configuration
In addition to service configuration, Symfony also has configuration for its MVC/URL routing. Philosophically, Symfony views service configuration as an entirely different thing than the routing configuration. These are separate systems — that’s why service configuration goes in service
.(yml|xml|php
), and routing configuration goes in routing
.(yml|xml|php
).
However, the borders between the two systems aren’t as clear as you’d think. When we answered the configuration question
Configuration format (yml, xml, php, or annotation) [annotation]: yml
We were telling our bundle to use the yml
format for both routing and service configuration. Not that big a deal, until we consider the default annotation
configuration format.
What is the annotation
format? Unlike yml
, xml
, and php
, the annotation
format does not rely on external configuration files. Instead, a special syntax is used in PHP doc-blocks. You can see an example of a routing annotation in the Symfony manual
/**
* @Route("/{id}", requirements={"id" = "\d+"})
*/
public function showAction($id = 1)
{
}
and some examples of dependency injection configuration in this file.
Covering annotations is beyond the scope of this article, and I’d recommend you stay away from them when you’re starting Symfony development. They’re conceptually interesting — but it’s also a step beyond normal configuration. That is, learning to configure Symfony’s routes and services is already a tricky thing if you’re never done it before, and annotations will only muddy the waters further.
For example, despite choosing annotation
as the format, the generation code still creates a service configuration file. If we had chosen annotation
, the PulsestormHelloworld2Extension
object would still load a service.xml
file. Even though we chose annotation
as the format, Symfony still created an XML configuration file. Also, the Doctrine
ORM system (used by OroBAP/Symfony) uses annotations as well, but these annotation have nothing to do with routing or service configuration.
As you become conversant in Symfony’s dependency injection and routing features, you’ll start to understand the problem annotations are trying to solve. If you’re just getting stared through, it’s headache inducing. Once you have basic Symfony configuration under your belt then consider taking a look at annotations. We only mention them here so you’ll know it’s ok to avoid them for now, but that you’ll recognize them when you see them in other people’s code.
Wrap Up
Today we covered using the Symfony console application to automate bundle creation. This is only one of the many tasks the application console can help you with — think of it as your Symfony swiss army knife.
In exploring full bundle creation, we (unavoidably) stumbled head first into the deeper world os Symfony’s service container architecture. In our next article, we’ll cover the basics of service configuration, and why we’ll want to go to the extra trouble for our Oro applications.