Just a quick, but important, site announcement. This site, Oro Quickies, is now located at http://oro-quickies.alanstorm.com. Updates will (and have) ceased here. You’ll want to update your bookmarks (after crank starting your car), and/or add the feed at http://oro-quickies.alanstorm.com/rss/ to receive future updates.
OroCRM requires PHP 5.4
I was surprised to see this when running through the new OroCRM Wizard
It shouldn’t be a big deal — PHP 5.3 is nearing end of life after all — but it’s so common to run across a Magento shop which requires PHP 5.2 that it was a bit of a shock to see something modern. I recommend php-osx for Mac users who haven’t embraced virtual machine based development.
OroCRM and Akeneo Beta Releases
Both OroCRM/OroBAP and Akeneo (the PIM product based on OroBAP, whose developers are working closely with the OroCRM company) have reached their first betas. It is, of course, impossible to tell what anyone means by a beta anymore, but both projects seem well on their way to a 1.0.
Interestingly, they both still use the GitHub/PHP-Composer method of installation. OroCRM does have a wizard interface, but you still need to grab the code via GitHub, and dependencies via Composer. Hard to tell if this is a strategy to hold the products from general consumers until an official 1.0, or if it’s going to be The Way Things are Done™ for non-cloud based applications from now on.
Alpha 4 Installation Errors
When I was installing the new OroCRM alpha (with composer), the post-installation process bailed with the following.
Unrecognized options "use_aop, default_class" under "a2lix_translation_form"
I’m not familiar with the a2lix_translation_form
bundle, but I was able to fix the problem by opening the Symfony application configuration
app/config/config.yml
and adding the following comments (yaml
files use #
as a commenting character; perl people, go figure)
a2lix_translation_form:
locales: [en, fr]
default_required: true
templating: "OroUIBundle:Form:translateable.html.twig"
# use_aop: false
# default_class:
# service: "A2lix\TranslationFormBundle\TranslationForm\DefaultTranslationForm"
# listener: "A2lix\TranslationFormBundle\Form\EventListener\DefaultTranslationsSubscriber"
# types:
# translations: "A2lix\TranslationFormBundle\Form\Type\TranslationsType"
# translationsFields: "A2lix\TranslationFormBundle\Form\Type\TranslationsFieldsType"
If this were a 1.0 release we’d probably want investigate the reasons for this a little more — but since this is still an alpha release it’s probably safe to assume things will shake out as an official release gets closer.
Link
Thus starts the long march through a new MVC framework’s features and quirks.
What’s an AbstractEntityFlexible Model Entity?
When I first started diving into the OroCRM codebase, I was a little confused by the model class declarations
class Account extends AbstractEntityFlexible
{
}
The AbstractEntityFlexible
threw me. I’d heard OroBAP was using Doctrine as their ORM, but this extending the AbstractEntityFlexible
made is seem like they’d implemented their own ORM. By default, Doctrine entity classes are simple classes that don’t extend anything.
[The Model], often called an “entity”, meaning a basic class that holds data – is simple and helps fulfill the business requirement of needing products in your application. This class can’t be persisted to a database yet – it’s just a simple PHP class.
Additionally, When I poked around the database I saw a multi-table-per-entity setup. This helped reenforce that assumption.
Fortunately, a bit of research pointed me right. The AbstractEntityFlexible
class creates a number of standard helper methods for OroBAP objects (shortcuts for getting data, compatibility methods for twig, etc.), and nothing more.
As for the multi-table setup, this is still Doctrine. OroBAP uses a custom doctrine repository (Oro\Bundle\FlexibleEntityBundle\Entity\Repository\FlexibleEntityRepository
). This appears to be what implements the multiple-table approach to storage of OroBAP model entities. I’m still getting into this system, but so far it looks like EAV-lite. More to come as my tire kicking continues.
PHP Automatic Object Casting
This one isn’t specific to Oro or Symfony, it’s just a part of standard Object Oriented PHP.
I’m digging through Symfony’s kernel booting code, and I came across this
$cache = new ConfigCache($this->getCacheDir().'/'.$class.'.php', $this->debug);
...
require_once $cache;
The variable $cache
contains an object, but then it’s being used by require_once
? How does that even make sense? What’s happening here is PHP’s automatic type juggling.
Small detour! In PHP, we can concatenate a string and an integer
$string = 'Foo';
$int = 1;
$bar = $string . $int;
In other languages, this sort of thing would be illegal. The value of $string
is a string, the value of $int
is an integer, and concatenating something that’s not a string is not allowed. PHP, however, infers we meant to cast the $int
as a string
$bar = $string . (string) $int;
In general, if you use a non-string in a string context, PHP will automatically cast the non-string to a string. So the require_once
above might be more explicitly written as
require_once (string) $cache;
since the require_once
function expects a string as its first parameter.
However, that still leaves the question: What should casting an object as a string do? Try something like this
$foo = new stdClass;
echo $foo,"\n";
and PHP will yell at you with an error like this.
PHP Catchable fatal error: Object of class stdClass could not be converted to string in …
All the traditionalist are nodding their head, glad PHP finally got something right. However, user defined objects have an optional magic method named __toString
. Programmers may create a __toString
method in their objects to define what should happen when their object is treated as a string.
That’s what’s happening in the Symfony code above. The cache object has a __toString
method that looks like this
#File: vendor/symfony/symfony/src/Symfony/Component/Config/ConfigCache.php
public function __toString()
{
return $this->file;
}
So, when you cast this object as a string, it returns the values of its file
property (which is a string). A clever technique for sure, but one that’s used rarely enough it always makes me stop for 10 seconds an go “huh?” before I remember “Oh yeah, go find the __toString
method”.
OroUI Built in Documentation
I’m working on my first full length Oro tutorial, and I stumbled across this tidbit. In a controller action method, try rendering the OroCRMAccountBundle::layout.html.twig
template.
public function indexAction()
{
$view = $this->render('OroUIBundle:Default:index.html.twig');
return $view;
}
Load your page, and you’ll get this
A list of links with examples of the default layouts provided by the OroBAP framework. You can get the specific twig template used for a layout by looking at the OroUiBundle
routing file
#File: vendor/oro/platform/src/Oro/Bundle/UIBundle/Resources/config/routing.yml
oro_ui_index:
pattern: /
defaults: { _controller: FrameworkBundle:Template:template, template: "OroUIBundle:Default:index.html.twig" }
...snipped...
and matching up URL patterns with templates.
Ack and Twig Templates
I’ve long advocated using the ack
command line program to search through the source code of large projects. In general, it’s smart about what it should and shouldn’t search (skips .git
and .svn
, for example), and it provides better formatted output for matches.
However, ack
won’t search .twig
templates by default. You need to add them as a type. To do this, add an .ackrc
file to your home directory with the following contents
--type-set=twig=.twig
This will tell ack
that .twig
files are a real thing and it should search them.
Oro Backbone Navigation Cache
Speaking of the Oro.Navigation
class, that’s also where the client-side ajax caching happens.
#File: vendor/oro/platform/src/Oro/Bundle/NavigationBundle/Resources/public/js/hash.navigation.js
savePageToCache: function(data) {
if (this.contentCacheUrls.length == this.maxCachedPages) {
this.clearPageCache(0);
}
var j = this.contentCacheUrls.indexOf(this.removePageStateParam(this.url));
if (j !== -1) {
this.clearPageCache(j);
}
this.contentCacheUrls.push(this.removePageStateParam(this.url));
this.contentCache[this.contentCacheUrls.length - 1] = data;
},
Since maxCachedPages
is initially set to 2
#File: vendor/oro/platform/src/Oro/Bundle/NavigationBundle/Resources/public/js/hash.navigation.js
maxCachedPages: 2,
this means OroCRM and OroBAP will cache the last two ajax navigation requests. If you want to clear that cache, you’ll need to click around. Unfortunately, because the navigation object is instantiated in a local function scope,
#File: vendor/oro/platform/src/Oro/Bundle/UIBundle/Resources/views/Default/index.html.twig
$(function() {
//...
new Oro.Navigation({baseUrl : 'http://oro.example.com'});
//...
});
it’s impossible (as far as I know) to access this object and clear the cache ourselves using the methods on the Navigation
object.