![]() |
|
Snippets |
|
Doctrine's built-in support for multiple connections follows a baffling use-case that allows different Model objects to be saved on different connections. A more common usage of multiple connections is to send read commands to a slave database and write commands to a master. Here's how to do it.
Declare your connections in databases.yml as usual, name one of them master and one of them slave.
Override Doctrine's default Query and Record objects. Edit config/ProjectConfiguration.class.php and add the following method:
public function configureDoctrine(Doctrine_Manager $manager) { // Configure custom query and custom record classes $manager->setAttribute(Doctrine::ATTR_QUERY_CLASS, 'MyQuery'); $options = array('baseClassName' => 'MyRecord'); sfConfig::set('doctrine_model_builder_options', $options); }
Create your MyQuery class in lib/MyQuery
class MyQuery extends Doctrine_Query { public function preQuery() { // If this is a select query then set connection to the slave if (Doctrine_Query::SELECT == $this->getType()) { $this->_conn = Doctrine_Manager::getInstance()->getConnection('slave'); // All other queries are writes so they need to go to the master } else { $this->_conn = Doctrine_Manager::getInstance()->getConnection('master'); } } }
Create your MyRecord class in lib/MyRecord.class.php
abstract class MyRecord extends sfDoctrineRecord { public function save(Doctrine_Connection $conn = null) { $conn = Doctrine_Manager::getInstance()->getConnection('master'); parent::save($conn); } }
If you're using a Doctrine behaviour that creates new tables, like the Versionable behaviour, tell it to use MyRecord too.
Entity:
ActAs:
Versionable:
builderOptions:
baseClassName: MyRecord
Finally, rebuild your model
./symfony doctrine:build --all-classes
If you have multiple slaves, modify the ProjectConfiguration to pick one on startup then change the method on your Query object to retrieve it.
This snippet allows you to evaluate functions speed and display them in symfony debug bar.
$timer = sfTimerManager::getTimer ('ldap_request'); // Create and start a new timer // do some stuff $timer->addTime (); // stop and store the timer value
Environnement : Symfony 1.2 - sfDoctrinePlugin
Context : doctrine:generate-admin modules
Problème : filters case-sensitive
Objective : make filters case-unsensitive
Every classes inherited from sfFormFilter alse inherite from BaseFormFilter, which is modifyable in every project. In the Doctrine case, sfFormFilterDoctrine and BaseFormFilterDoctrine. This class "Base" is here :
lib/filter/doctrine/BaseFormFilterDoctrine.class.php
The aimed method is the one which works on text fields. The line we want is the line 209 of the file symfony/lib/plugins/sfDoctrinePlugin/lib/form/sfFormFilterDoctrine.class.php :
$query->addWhere('r.' . $fieldName . ' LIKE ?', '%' . $values['text'] . '%');
So a solution should be the override this method replacing LIKE by ILIKE. Let's try :
Overriding the class sfFormFilterDoctrine's method addTextQuery() should give you this kind of stuff if there is no more work done on it :
# lib/filter/doctrine/BaseFormFilterDoctrine.class.php /** * Project filter form base class. * * @packagefilters * * @versionSVN: $Id: sfDoctrineFormFilterBaseTemplate.php 11675 2008-09-19 15:21:38Z fabien $ */ abstract class BaseFormFilterDoctrine extends sfFormFilterDoctrine { public function setup() { } // totally inspirated from lib/vendor/symfony/lib/plugins/sfDoctrinePlugin/lib/form/sfFormFilterDoctrine.class.php // function addTextQuery() // line modified : 209 // be careful on symfony updates to update this method if there are changes protected function addTextQuery(Doctrine_Query $query, $field, $values) { $fieldName = $this->getFieldName($field); if (is_array($values) && isset($values['is_empty']) && $values['is_empty']) { $query->addWhere('r.' . $fieldName . ' IS NULL'); } else if (is_array($values) && isset($values['text']) && != $values['text']) { // Here is the change, with "ILIKE" instead of "LIKE" $query->addWhere('r.' . $fieldName . ' ILIKE ?', '%' . $values['text'] . '%'); } } }
The original documentation in french is available here :
http://www.e-glop.net/main/Filtres_case-unsensitive_dans_Symfony
As of Symfony 1.1, using alternative formats has become simple. However, automatic use of these formats is now disabled by default.
Add support for sf_format in your routing factory :
/apps/myapp/config/factories.yml
all:
routing:
...
param:
...
suffix: .:sf_format
This way, each time you go to http://mysite/module.format, the corresponding format will be detected and used.
Following URLs will automatically work and be rendered as XML : - http://mysite/module.xml # renders indexSuccess.xml.php - http://mysite/module/action.xml # renders actionSuccess.xml.php - http://mysite/module/action/param/value.xml # equals http://mysite/module/action.xml?param=value
If you want the suffix not to be mandatory (e.g. http://mysite/module/action == http://mysite/module/action.html) you'll have to add "sf_format: html" in you routing rules parameters. This is required if you want your old calls to link_to() and url_for() (who don't specifies sf_format parameter) still work.
/apps/myapp/config/routing.yml
default: url: /:module/:action/*
myTemplate.php
link_to('hey !', 'module/action') // Produces an error ! link_to('hey !', 'module/action?sf_format=html') // Works, but hey, you'll have to add this to *all* your links :/
Add the default format :
/apps/myapp/config/routing.yml
default:
url: /:module/:action/*
param: { sf_format: html }
myTemplate.php
link_to('hey !', 'module/action') // Works :) link_to('hey !', 'module/action?sf_format=xml') // Works too, of course :)
We can use a filter, or plug to an even in the ProjectConfiguration class. My choice was a filter.
/apps/myapp/config/filters.yml
# insert your own filters here detect_format: class: DetectFormatFilter
/lib/DetectFormatFilter.php
class DetectFormatFilter extends sfFilter { public $default_formats = array( 'html' => array('text/html', 'application/xhtml+xml'), ); public function execute($filterChain) { $request = $this->context->getRequest(); // Only if format is not already defined if (!$request->getRequestFormat()) { // Complete with the filter default formats foreach ($this->default_formats as $format => $mimeTypes) { foreach ($mimeTypes as $mimeType) { if (!$request->getFormat($mimeType)) { $request->setFormat($format, $mimeType); } } } // Detect format depending on acceptable contant types : first is prefered foreach ($request->getAcceptableContentTypes() as $mimeType) { if ($format = $request->getFormat($mimeType)) { $request->setRequestFormat($format); break; } } } // execute next filter $filterChain->execute(); } }
Take a look at the fact we automatically add "html" to the formats, attached to the two standard mime types. This could be done by adding the corresponding options to the request part of factories.yml, but I wanted this filter to be "ready-to-use" without touching other parts of configuration. "text/html" must be handled apart because as "html" is the default format, Symfony just does not handle it. So when we browse "Accept" headers, we will pass on "text/html", and just skip it because it's not configured. Without adding this part, "html" format would never been rendered, quite annoying isn't it ;)
With this configuration, you can just add the "Accept" header to say you want an XML content, and your ".xml.php" templates will then be rendered.
An interesting evolution of this filter would be to allow it to test if the format can be rendered, and if not, fall back to a default one.
It's quite easy to enable back auto-detection of the format. However, as Fabien says in the ticket 4920, Accept header is sometimes unreliable, and some badly configured browsers could make you think they prefer XML, which will not be the case of the user behind this browser ;)
That said, even if the URL suffix method is probably the best choice, this auto-detection method works and could be useful in some projects...
[UPDATE]
I've found a cleaner way to deal with it :
sfProjectConfiguration::getActive()->getEventDispatcher()->notify(new sfEvent($this, 'foo.bar'))
The old version of this snippet is now useless, you don't need to read it.
[/UPDATE]
This snippet is not a tutorial on symfony events, but you can find some documentation in the book and or in this good tutorial.
The event dispatcher is a very powerfull tool, but you might meet the same issue than me if you want to use it from a Model Class.
Let's take the classic Author/Article project sample.
When an article is added, it might be interesting to trigger an "article.new" event. This event coud be listened by different actors.
Like for instance :
The most obvious place to trigger the event is the Article save() method. Here is the code for Doctrine (the logic is exactly the same for Propel):
Article.class.php
public function save(Doctrine_Connection $conn = null) { if ($this->isNew()) { // Let's trigger the event sfContext::getInstance()->getEventDispatcher()->notify(new sfEvent($this, 'article.new')); } return parent::save($conn); }
Unfortunatly, this code won't work. If you try the "symfony doctrine:build-all-reload" command, here is the error that will be displayed :
Cli
sfException: The "default" context does not exist.
Why ? Because symfony tasks don't initialise the sfContext class. And for the moment (symfony version 1.2.4) I don't think there's a clean way to access to the dispatcher object from a Doctrine Class.
Here is my solution : let's build a (very) little singleton class that will provide the dispatcher ressource everywhere in the project, and even when sfContext is not avaible
yourProjectRoot/lib/Fedex.class.php
class Fedex { static protected $instance; protected $dispatcher; /** * The singleton logic http://en.wikipedia.org/wiki/Singleton_pattern#PHP_5 */ static public function getInstance() { if (!self::$instance instanceof self) { self::$instance = new self; } return self::$instance; } public function setEventDispatcher(sfEventDispatcher $dispatcher) { $this->dispatcher = $dispatcher; } public function getEventDispatcher() { return $this->dispatcher; } }
Of course, you first have to give to the Fedex class a link the delivery ressource. Let's do it with the very first class called by the script of the project :
yourProjectRoot/config/ProjectConfiguration.class.php
public function setup() { // add after all your setup stuffs require_once(dirname(__FILE__).'/../lib/Fedex.class.php'); Fedex::getInstance()->setEventDispatcher($this->dispatcher); }
That's it, you're done. The Fedex class will deliver the dispatcher ressource everywhere in your project through Fedex::getInstance()->getEventDispatcher() And it works even when the sfContext class is not avaible
Article.class.php
public function save(Doctrine_Connection $conn = null) { if ($this->isNew()) { Fedex::getInstance()->getEventDispatcher()->notify(new sfEvent($this, 'article.new')); } return parent::save($conn); }
The important thing is that this little class doesn't modify any part of the symfony event system. It just provide you an alternative way to access to it.
<link rel="canonical" href="<?php echo url_for($sf_context->getRouting()->getCurrentInternalUri(true), true); ?>" />
[Edit 06/02/09] Enabling the apache mod_rewrite is a little faster than using this snippet. ;)
Well if you are using Windows + Wamp for your dev, the "noscript" setting (settings.yml) does not work well when you are using the prod controller to test your application with you dev computer. (if mod_rewrite is not installed)
So i wanted to deactivate this settings in dev environment with the prod controller and having it on in prod with the prod controller without having 2 different settings.yml files.
So here here a small tip for this:
prod:
.settings:
no_script_name: <?php echo ((isset($_SERVER['HTTP_HOST']) && strstr($_SERVER['HTTP_HOST'], 'dev') ? 'off' : 'on')) . "\n"; ?>
logging_enabled: on
This code assumes that your dev url contains "dev" like http://dev.blogsnippets.com.
Hi!
I am using a lot of hand written jscript codes due to the required behavior not found in any javascript framework. I've created my Objects in distinct files, although some of them are quite small ( <1kb ). So to make it fast, I've written a filter class that actually parses the files found in the /js directories having *.js suffix. It does not only parse them together but removes whitespaces, comments, indentations so the sum filesize gets a bit compressed. The filter also includes the final file into the response so there is no need to include any *.js ( inside /js ) in a template.
<?php class JavascriptParser extends sfFilter { public function execute($filterChain) { $filterChain->execute(); $fp = fopen( "js/compiled.js", "w" ); JavascriptParser::getJSFiles( "js", $fp ); fclose ( $fp ); $response = $this->getContext()->getResponse(); $content = $response->getContent(); if (false !== ($pos = strpos($content, '</head>'))) { $html = "<script type='text/javascript' src='/js/compiled.js'></script>"; if ($html) { $response->setContent(substr($content, 0, $pos).$html.substr($content, $pos)); } } } public function getJSFiles( $dir, &$fp ) { $hDir = opendir( $dir ); while( ( $filename = readdir( $hDir ) ) !== false ) { if ( is_dir( $filename ) ) { } else if ( is_dir( $dir."/".$filename ) ) JavascriptParser::getJSFiles( $dir."/".$filename, $fp ); else if ( strpos( $filename, ".js" ) !== false && $filename != "compiled.js" ) { $tmpFile = fopen( $dir."/".$filename, "r" ); $data = fread( $tmpFile, filesize( $dir."/".$filename ) ); $data = preg_replace( "'\/\*.*?\*\/'si", "", $data ); $data = preg_replace( "'//.*?\n'si", "", $data ); $data = preg_replace( "'[ \t]+'", " ", $data ); fwrite( $fp, " \n".$data ); fclose( $tmpFile ); } } closedir( $hDir ); } } ?>
The same should be done with *.css files, since they are even smaller and loading many small files takes much more time then loading one big.
Best Regards
Here is how I did this... Create a sfGuardAuth module in your application and edit the actions.class.php file as follow.
The trick is to not try to overwrite the sfGuardAuth/signin action, as it use validation. As well it allow you to use the "normal" signin way (form and etc).
require_once(sfConfig::get('sf_plugins_dir').'/sfGuardPlugin/modules/sfGuardAuth/lib/BasesfGuardAuthActions.class.php'); class sfGuardAuthActions extends BasesfGuardAuthActions { public function executeHTTPSignin() { // get somme interesting stuff! $request = $this->getRequest(); $response = $this->getResponse(); $user = $this->getUser(); // An HTTP authenticated user cannot logout (browser always send authentification datas) // So we must be sure that the user has seen the HTTP authentification box before if ( $user->getAttribute('request_authentification') ) { // If authentification datas has been sent if ( isset( $_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW'] ) ) { // If correct username given $guarduser = sfGuardUserPeer::retrieveByUserName( $_SERVER['PHP_AUTH_USER'] ); if ( $guarduser instanceof sfGuardUser ) { // If correct Password given if ( ($guarduser instanceof sfGuardUser) and ($guarduser->checkpassword( $_SERVER['PHP_AUTH_PW'] )) ) { // we can signin the user and redirect it $user->signin( $guarduser ); $user->setAttribute('request_authentification',false); $this->redirect( sfConfig::get('app_sf_guard_plugin_success_signin_url','@homepage') ); throw new sfStopException; } } } } // else, popup the authentification box $user->setAttribute('request_authentification',true); $response->setHttpHeader( 'WWW-Authenticate', 'Basic realm="Identification"' ); $response->setHttpHeader( 'HTTP/1.0', '401 Unauthorized' ); // This will be displayed if the user cancel the authentification process $this->forward( 'sfGuardAuth', 'password' ); throw new sfStopException; } public function executePasswowd() { # Implement this action as usual... } }
Enjoy... (I hope)
I made a small more general modification to the edit_in_place update action, you can use that in any action
class myTools { /** * performs update on any single column for ajax actions * * @param string $peer * @param integer $id * @param string $field * @return object */ public static function updateField($peer, $id, $field, $value) { if (!class_exists($peer)) { throw new InvalidArgumentException($peer.' does not exist'); } $method = new ReflectionMethod($peer, 'retrieveByPk'); $object = $method->invoke(NULL, $id); $object->setByName($field, $value, BasePeer::TYPE_FIELDNAME); $object->save(); return $object; }