Localizing your Twig-using Zend Framework 2 Application with PoEdit
Quickstart to getting translations into your ZF2 applications using Poedit, and TWIG. This can be a real PITA, and has a few gotchas since you're using a few bits of open source software. It can be put together to work though, here's how.
This may seem like yet-another-PoEdit post, but, I write it because I hadn't found any that brought Zend Framework 2 and Twig together in a stepwise fashion that made sense to someone who's just looking to integrate translations into their ZF2 apps. If you're doing it under the gun, and fire up POEdit quickly because you "usually do" figure things out, you're likely to !%!@#^@#$@… [and I did for awhile too, bug in Poedit that's enough to make you double up on your Lisinopril].
Getting Ready
- Download PoEdit if you haven't already.
- Require Twig-Gettext-Extractor by adding "umpirsky/twig-gettext-extractor": "dev-master" to your composer.json file (and run your composer update command thereafter)
- Install gettext on your system. If command 'xgettext' doesn't work from your Terminal, you need to install it (edit: added these instructions at the end if you need 'em).
EDIT: I uncovered a few minor gremlins in the umpirsky extractor, I forked it and tweaked ever so slightly to make your life happy again.
Configuring PoEdit
Download PoEdit. I'm doing this from a Mac, so your mileage may vary a tiny bit. If you are a Mac user like me, using version 1.6, there's a pesky bug that makes TMP files vanish. You can get around this by launching Poedit from the terminal with:
WXTRACE=poedit,poedit.tmp,poedit.execute /Applications/Poedit.app/Contents/MacOS/Poedit --verbose --keep-temp-files
- Go to PoEdit > Preferences > Personalize. Fill in your name and email.
- Still in this Preferences panel, go to Parsers
- PHP should already be in there, select it and click Edit.
- In the panel that appears, change your list of extensions to *.php, *.phtml
- Click OK to close
- Now to add a new parser for Twig templates, click New
- Configure as such:
Language | Twig |
List of extensions | *.twig |
Parser command | /path/to/your/project/vendor/bin/twig-gettext-extractor --sort-output --force-po -o %o %C %K -L PHP --files %F |
An item in keywords list | -k%k |
An item in input files list | %f |
Source code charset | --from-code=%c |
- Click OK
- Click OK again, that should close the preferences dialog.
In debugging, I symlinked twig-gettext-extractor into /usr/local/bin. Simplifies the whole affair for the configuration of separate catalogs. If you do this, you can simplify the parser command considerably.
Creating a Catalog
- Go to File > New Catalog -- this opens a new settings panel
- Fill out the Translations Properties panel completely* , setting Charset and Source Code Charset to UTF-8 (this is likely right, tweak if need be). * you can ignore plural forms for now.
- Head over to the Paths tab
- Put a dot in Base Path
- In the Paths box on the bottom of this tab, use the GUI (throwback to Windows 95 here) to add your full folder paths (it's the only way I could get it to work, Base Path seems to break things for me)
- Click on the Keywords tab, and set it to this list:
Sources keywords list
- Click OK
Update the Catalog
When you click OK, it should parse through and update the catalog. Note: I ran into a strange bug with version 1.5.7 on OSX where it wouldn't parse unless I started the application from the command line - it's the line I noted above. Tinker with the UI and put your strings in. I had my en_US.po file saved in my module's language folder, you should too. When you save your "catalog", it'll crunch down a .po and a .mo. Now you just need to tell your ZF2 app to use them.
Module Configuration
I've read a few different ways of doing this online, I settled on this one and it works. In your module.config.php, make sure you have this config array in place:
module.config.php
'translator' => array(
'locale' => 'en_US',
'translation_file_patterns' => array(
array(
'type' => 'gettext',
'base_dir' => __DIR__ . '/../language',
'pattern' => '%s.mo',
),
),
),
This tells your module how to behave: what type of translator to use, and where to find the gettext .mo file.
Then, you need to set the translator up. This is done in your Module.php inside of the onBootstrap block. In my case, I have a series of stacked libraries that each have their own translator setups, and have only changed the Module.php for my core "Application" module.
Module.php::onBootstrap
$translator = $e->getApplication()->getServiceManager()->get('translator');
$translator->setLocale( ( isset( $_COOKIE['locale'] ) ? $_COOKIE['locale'] : 'en_US' ) )
->setFallbackLocale( 'en_US' );
I use cookies to switch languages (via URL that sets cookies), so I've set it up to use the locale cookie plainly, with en_US as the basic locale (this is the name of your catalog btw).
Puttin' It All Together
You can use these translations from anywhere! Controllers, with:
$translator = $this->getServiceLocator()->get('translator');
$translator->translate( "Some String In Your Catalog" );
In discrete objects, purists in the crowd will argue that you should be using DI to pass the translator into the objects that need it, don't necessarily rely on the availability of the service locator. I think the availability of the SL is a hot topic with ZF3.
Most important, is its use in templates. Great examples here http://twig.sensiolabs.org/doc/extensions/i18n.html
Compiling Gettext
If you're on Windows, LMGTFY.
Homebrew has a convenient brew install gettext
Lastly, it's pretty painless to handbomb-in on most awesome OSes:
- Download Gettext from http://www.gnu.org/software/gettext/
- Extract
- ./configure
- sudo make && make install
Done like dinner.
Related Reading
- http://samminds.com/2012/09/create-po-language-files-using-poedit/
Solve "MySQL server has gone away", Doctrine2 & Zend Framework 2
Doctrine2 and Zend Framework 2 are a recommended relationship, but if you have a long-running task or daemon that's dying because of connection loss, this article might help.
If you're coding with Zend Framework 2, and have adopted Doctrine 2 for all the right reasons, you've probably done a good deal of brain bending to rid yourself of table gateway thinking, to set up your Entities with proper annotations, and have done some slick DI to inject the right dependencies in the right places. This was pretty much the path I'd taken, thoroughly embedding the lot into a framework I've been developing for a few months.
Integral to that framework, is a whole reporting aspect that blends entities from many of my ZF Modules, crunches up a bunch of good stats for my partners, co-workers and clients, and drops them into an AWS Redshift BI warehouse for later grinding. I'd contend that I had done everything by the book, using Memcache and Gearman to parallelize data gathering with these PHP jobs, to one Monday roll into work and see a stalled status flag: a big old 2006 in one of the Gearman task logs.
I immediately guessed that the connections being used by Doctrine were dying with no auto-reconnect, and Googled very quickly thinking "Someone out there has to have solved this." Makes sense, it's related to wait_timeout; but how to fix?
Everything I read at that point pointed to using a dummy select in conjunction with some try catch blocks to assert the connection's heartbeat. Coded like such:
/**
* Assert/Revive a Doctrine repository connection
* @param \Application\Mapper\AbstractDoctrineMapper $R Doctrine repository
* @throws PDOException
*/
protected function assertDatabaseRepositoryConnection(\Application\Mapper\AbstractDoctrineMapper &$R)
{
$EM = $R->getEntityManager();
$DBC = $EM->getConnection();
// connections die because of process length sometimes, have to kick them back into shape
try {
$DBC->query("SELECT 1");
if (self::DEBUG)
$this->setStatus(" ++ EM connection was alive, continuing");
} catch (\Exception $e) {
if (self::DEBUG)
$this->setStatus(" ++ EM connection had died. Reviving.");
$newEM = $this->getServiceLocator()->create('doctrine.entitymanager.orm_default');
if (self::DEBUG)
$this->setStatus(" ++ Created New EM.");
$R->setEntityManager($newEM);
}
}
I tested locally, writing a few unit tests to make that select fail to ensure that the connection would be recreated with a new SPL hash, and it was. Seemed right, so the code began its march up our GIT repositories, right to our stat-grinding fleet. Reset, start, wait...
Approximately 2 days later, same old "MySQL server has gone away" error. Poking some more, an IRC chat on Freenode's #doctrine had someone identify that may be caused by Zend Framework's shared services. I edited my \Application's getServiceConfiguration() and set shared to false for my database orm connection.
public function getServiceConfig()
{
return array(
'invokables' => array(
...
),
'initializers' => array(
...
),
'factories' => array(
...
),
'shared' => array(
'doctrine.entitymanager.orm_default' => false
),
);
}
You guessed it. I run the Unit Tests, things seem fine (no difference in fact), so I push it up to our cloud and restart the process. 2 days later, same darned thing!
It was probable at this point that the mass of queries I was running at such irregular intervals caused the Dummy SQL to validate, but the connections would die in the time that exists between the Dummy SQL and the ER commit. That or gremlins. Trying to find documentation for connection wrappers for Doctrine 2, I found an enhancement request on the Doctrine portal that was pretty darned close to what I needed. Do I think this should be a standard option too? OH YES! In the interim though I adjusted the older files at that link so that they comply with the version of Doctrine 2 that I am using. It's been in production usage for about a day now, and it's working well, so I wanted to give back & share. Big thanks to Marco Pivetta from Freenode's #Doctrine for chipping in, and to Dieter Peeters for writing up that JIRA thread & code. Here's my take on it:
1. Adjust your Doctrine DB Config
'doctrine' => array(
'connection' => array(
'orm_default' => array(
//'driverClass' => 'Doctrine\DBAL\Driver\PDOMySql\Driver',
'driverClass' => '\Application\Model\Doctrine\DBAL\Driver\PDOMySql\Driver',
'wrapperClass' => '\Application\Model\Doctrine\DBAL\Connection',
'params' => array(
'host' => 'host',
'port' => '3306',
'user' => 'user',
'password' => 'password',
'dbname' => 'dbname',
'driverOptions' => array(
'x_reconnect_attempts' => 10,
),
)
)
)
),
2. Install the Following Connection and Driver Wrappers
Click here to download the source code
Note: Updated on Sep 16, included more verbose error output, and a "stall retrying" condition for when our ISP's DNS would hit the proverbial fan.
3. Keep Calm, and Use Doctrine Normaly
No need for example, for the database connection assertion routines that are recommended in quite a few posts you'll find on this subject, that I pasted at the start of this article. It was a nice feeling to peel that stuff out of the code.
This does get rid of the problem, and given the nature of the sandbox where these problems surface, it's usually a few days in the toilet if you're digging blind. I hope this helps! One note, it only works for statements outside of transactions. That was a moot point for me given the nature of the long tasks.
Capturing auth events with ZfcUser
Had a hell of a time trying to find how to tap into ZfcUser's auth event (the registration events are really well documented, but this one...). Figured I'd write about it while the glue-it-back-hair-glue gently massaged onto my scalp dries.
I had a requirement to tap into ZFCUser's auth event to log IP addresses and times in addition to auth record ID. At the time at which this blog post was authored, documentation was nonexistent. If you've stumbled here, you're probably commiserating after having Googled and found dead-end StackOverflow posts!
Here's the solution! (Assuming you've already set up a custom user, see this blog post)
In your custom user Module's (e.g., FooUser per link above) onBootstrap event, add this code:
$sm = $e->getApplication()->getServiceManager();
$zfcAuthEvents = $e->getApplication()->getServiceManager()->get('ZfcUser\Authentication\Adapter\AdapterChain')->getEventManager();
$zfcAuthEvents->attach( 'authenticate', function( $authEvent ) use( $sm ){
try
{
$remote = new \Zend\Http\PhpEnvironment\RemoteAddress;
$remote->setUseProxy( true );
$mapper = $sm->get( 'auth_log_mapper' );
$log_entity = new \FooUser\Entity\UserAuthenticationLog;
$log_entity->populate(
array(
'user_id' => $authEvent->getIdentity(),
'auth_time' => new \DateTime( "now" ),
'ip_address' => $remote->getIpAddress()
)
);
$mapper->save( $log_entity );
return true;
}
catch( \Exception $x )
{
// handle it
}
});
Your mileage may vary, I left the guts of my custom user functions in there in case it helps. The $authEvent->getIdentity() returns an integer; and if you don't return true, it breaks the chain.
So simple, what a PITA to deciper! Hope this helps someone - I'd be a day younger.
From ZF1 Zend_Db Queries to ZF2 \Zend\Db\Sql\Sql?
If you're converting over to ZF1 from ZF2, there's quite a few subtleties to bridge. I hope this helps you bridge the gap between the two. By no means thorough, I just wanted to highlight the close-but-not-quite elements that would helped me get kickstarted.
Also, if you're looking to connect two databases to ZF2, this will probably help.
If you're converting over to ZF1 from ZF2, there's quite a few subtleties to bridge. Most of the docs are pretty clear, but I found some aspects of Zend_Db's successor to be a bit more obscure. Zend_Db was a good friend, especially when came time to adopt query abstraction without going through table gateways. It was so easy (probably sounding little spoiled now) to do then what I wanted to do now, which is wire a connection outside of Doctrine, in order to connect to an AWS Redshift Cluster (which is awesome by the way) to execute straight queries.
What I Used To Do, That I Wanted Anew
$DB = Zend_Db::factory
(
'Pdo_Mysql',
array(
'host' => 'host',
'password' => $this->getDatabasePass(),
'username' => $this->getDatabaseUser(),
'dbname' => $this->getDatabaseName(),
'options' => array(
Zend_Db::AUTO_QUOTE_IDENTIFIERS
)
)
);
$S = $DB->select()->from( 'table', array( 'col1', 'col3' ) )
->where( 'col2=?', "O'Smith" )
->limit( 1 );
$R = $DB->fetchAll( $S );
You'll see below that things have taken a bit of a turn for the more complex. Reading the rationale in all the PRs on GitHub is a bit of a brain-bender, and when you use something like Doctrine for most of the day to day, this is probably a fringe-case thing.
Prepping The New Mistress (ZF2)
I first recommend that you wire your connection the ZF2 way. I've got Doctrine2 already setup in my autoload configuration file -- just need to add a few lines for configuration's sake.
config/autoload/database.local.php
You'll notice that I'm using Pgsql above, you may want to change if you're going to use MySQL for example - just need to change the driver string
This done, edit your Module config at the getServiceConfig() level to include a factory to dispatch the connection:
module/Application/Module.php
public function getServiceConfig()
{
return array(
'factories' => array(
// it's this part that matters
'redshift_stats' => function ($sm) {
$config = $sm->get('config');
$dbc = $config['redshift']['connection']['stats_default'];
$adapter = new \Zend\Db\Adapter\Adapter( $dbc );
return new \Zend\Db\Sql\Sql( $adapter );
},
// end, part that matters
),
);
}
(your mileage may vary)
Thusly is born the part that lets you do what's below, which is the equivalent to the first (I thought succinct!) ZF1 block we started with:
$SQL = $this->getServiceLocator()->get('redshift_stats');
$DBA = $SQL->getAdapter();
$S = $SQL->select()
->from( 'table' )
->columns( array( 'col1', 'col3' ) )
->where( array( "col2" => "O'Smith" ) )
->limit( 1 );
$sel_string = $SQL->getSqlStringForSqlObject($S);
$R = $DBA->query( $sel_string, $DBA::QUERY_MODE_EXECUTE );
Oh yea, fetchAll, fetchOne, and fetchPairs are all gone... (I ran across a bridge on GitHub someone wrote to try to bridge this extricated functionality).
Insertion Queries
Inserts and updates work a bit differently with ZF2 as well. The ZF1 insert and update would execute right away, you have to extract the string and execute it now. This can be a gotcha as you get started
With Zend Framework 1
$DB = /* get your connection */;
$DB->insert( 'table', array( 'col1' => 123 ) );
With Zend Framework 2
$DB = $this->getServiceLocator()->get('redshift_stats');
$ADAPTER = $SQL->getAdapter();
$I = $SQL->insert( 'table' );
$I->values( array( 'col1' => 123 ) );
$ins_string = $SQL->getSqlStringForSqlObject($I);
$results = $ADAPTER->query( $ins_string, $ADAPTER::QUERY_MODE_EXECUTE );
Concluding, I hope this helps you bridge the gap between the two. By no means thorough, I just wanted to highlight the close-but-not-quite elements that would helped me get kickstarted.
The verbosity of it all, and Zend\Db's lack of support for batch inserts (yes I prefer MySQL) really makes adopting the likes of Doctrine2 worthwhile (I find the Mappers turn out cleaner overall). I think this may have been the MO with ZF2, less ORM, more query abstraction -- please use something else like Doctrine2 for heavy -- scratch that -- usual lifting.
ZF2, Doctrine2, ZfcUser - customizing the registration form and storing the custom user entity
ZFCUser is a great module, really cleanly written, and while there are some modules out there that'll help you extend ZFCUser with custom profiles, my challenge was one where I had to retrofit an existing Doctrine entity for use with ZFCUser -- let me tell you it worked great.
There are a lot of posts on SO that have to do with granular pieces of extending the ZfcUser form, but nothing I could find that had a usable A to Z for Doctrine2 users. I added a post on the ZFCUser Wiki about the storage piece, and thought I'd throw it in here to help the next wandering coder.
Assuming that you've done two things:
- set up your application.config.php to include ZfcBase, ZfcUser, ZfcUserDoctrineORM
- created your own user Module (e.g., module/FooUser/) with your entities and schema already set up
Essentially, you have a working ZfcUser, but just need to know how to extend the form and store (quite a few reasonable tutorials out there if you're not here yet. Find one, set it up, and then come back).
/module/FooUser/config/module.config.php
<?php
return array(
'doctrine' => array(
'driver' => array(
// overriding zfc-user-doctrine-orm's config
'zfcuser_entity' => array(
'class' => 'Doctrine\ORM\Mapping\Driver\AnnotationDriver',
'paths' => __DIR__ . '/../src/FooUser/Entity',
),
'orm_default' => array(
'drivers' => array(
'FooUser\Entity' => 'zfcuser_entity',
),
),
),
),
'zfcuser' => array(
// telling ZfcUser to use our own class
'user_entity_class' => 'FooUser\Entity\User',
// telling ZfcUserDoctrineORM to skip the entities it defines
'enable_default_entities' => false,
),
);
This tells your module config to use the entities you've set up in /modules/FooUser/src/FooUser/Entity/User.php for example, as the default ZfcUser User entity.
/module/FooUser/Module.php
<?php
namespace FooUser;
use Zend\Mvc\MvcEvent;
class Module {
public function getConfig()
{
return include __DIR__ . '/config/module.config.php';
}
public function getAutoloaderConfig()
{
return array(
'Zend\Loader\StandardAutoloader' => array(
'namespaces' => array(
__NAMESPACE__ => __DIR__ . '/src/' . __NAMESPACE__,
),
),
);
}
public function onBootstrap( MVCEvent $e )
{
$eventManager = $e->getApplication()->getEventManager();
$em = $eventManager->getSharedManager();
$em->attach(
'ZfcUser\Form\RegisterFilter',
'init',
function( $e )
{
$filter = $e->getTarget();
// do your form filtering here
}
);
// custom form fields
$em->attach(
'ZfcUser\Form\Register',
'init',
function($e)
{
/* @var $form \ZfcUser\Form\Register */
$form = $e->getTarget();
$form->add(
array(
'name' => 'username',
'options' => array(
'label' => 'Username',
),
'attributes' => array(
'type' => 'text',
),
)
);
$form->add(
array(
'name' => 'favorite_ice_cream',
'options' => array(
'label' => 'Favorite Ice Cream',
),
'attributes' => array(
'type' => 'text',
),
)
);
}
);
// here's the storage bit
$zfcServiceEvents = $e->getApplication()->getServiceManager()->get('zfcuser_user_service')->getEventManager();
$zfcServiceEvents->attach('register', function($e) {
$form = $e->getParam('form');
$user = $e->getParam('user');
/* @var $user \FooUser\Entity\User */
$user->setUsername( $form->get('username')->getValue() );
$user->setFavoriteIceCream( $form->get('favorite_ice_cream')->getValue() );
});
// you can even do stuff after it stores
$zfcServiceEvents->attach('register.post', function($e) {
/*$user = $e->getParam('user');*/
});
}
}
Hopefully this helps another soul out! This is one way to slice the pie that woulda saved me a bunch of time.
Tack BjyAuthorize on top of this, and you're off to the races.
https://packagist.org/packages/bjyoungblood/BjyAuthorize
Good Luck!