![]() |
|
Snippets |
|
The list view of the admin generator currently always displays all defined object actions for each object. There is no way to display an object action for an object only if some condition on this object is met.
In order to extend the admin generator with this functionality only a small enhancement is required. You can either apply this change per module or create a new admin generator theme as described in the Symfony book.
The templates/_list_td_actions.php has to be extended to look roughly like this, depending on whether you already have your own modifications in there:
<?php if ($this->getParameterValue('list.object_actions')): ?> <td> <ul class="sf_admin_td_actions"> <?php foreach ($this->getParameterValue('list.object_actions') as $actionName => $params): ?> <?php if ( isset( $params['condition'] ) ): ?> [?php if ( <?php echo ( isset( $params['condition']['invert'] ) && $params['condition']['invert'] ? '!' : '') . '$' . $this->getSingularName( ) . '->' . $params['condition']['function'] ?>( <?php echo ( isset( $params['condition']['params'] ) ? $params['condition']['params'] : '' ) ?> ) ): ?] <?php endif; ?> <?php echo $this->addCredentialCondition($this->getLinkToAction($actionName, $params, true), $params) ?> <?php if ( isset( $params['condition'] ) ): ?> [?php endif; ?] <?php endif; ?> <?php endforeach; ?> </ul> </td> <?php endif; ?>
With this enhancement you can now use conditions for your actions in the generator.yml. The syntax should be pretty self-explanatory. An example would look like this:
object_actions:
subscribe: { name: Notify when changed, action: subscribe, icon: pencil_add.png }
condition:
function: isUserSubscribed
params: "$sf_user, 'test'"
invert: true
As you can see each object action now also takes a condition parameter, which again takes a number of parameter.
function is the function of the object that will be called. It will be evaluated as boolean. Required!params is a string that will be passed verbatim to the function, so if you need several parameters make sure to enclose them in quotes. Since the function will be called from the view layer you have access to all data available there such as $sf_user. Defaults to an empty stringinvert will invert the return value of the function so saving you an additional function if you need something like subscribe / unsubscribe actions. Defaults to false.Enjoy!
By default, I18n content in database does not support fallback in default culture. This snippet allow you to enable I18n content fallback in order to always have a default value for your texts.
This snippet is sponsored by Dorigo consultants.
To enable the fallback, edit your table object class in lib/model/TableClassName.php and add the following code. Then search and replace TableClassName by your table object class name.
/** * Fetch the i18n object for this object culture. * * @return mixed A i18n object * @throws PropelException Any exceptions caught during processing will be * rethrown wrapped into a PropelException. */ public function getCurrentTableClassNameI18n() { if (!isset($this->current_i18n[$this->culture])) { $obj = TableClassNameI18nPeer::retrieveByPK($this->getId(), $this->culture); if ($obj) // Test if there is a translation for current culture { $this->setTableClassNameI18nForCulture($obj, $this->culture); } else // Create a translation for this culture { $new_i18n = new TableClassNameI18n(); $default_culture = sfConfig::get('sf_i18n_default_culture'); // We try to fetch the default culture translation to initialise the new culture. if (!isset($this->current_i18n[$default_culture])) { $obj = TableClassNameI18nPeer::retrieveByPK($this->getId(), $default_culture); if ($obj) // Test if there is a translation for current culture { $this->setTableClassNameI18nForCulture($obj, $default_culture); } } else { $obj = $this->current_i18n[$default_culture]; } if ($obj) { $obj->copyInto($new_i18n); } $new_i18n->setId($this->getId()); $new_i18n->setCulture($this->culture); $this->setTableClassNameI18nForCulture($new_i18n, $this->culture); } } return $this->current_i18n[$this->culture]; }
You now need to add default translation when you create a new object. We do that with this doSave function.
To use this function, edit your table object class in lib/model/TableClassName.php and add the following code. Then search and replace TableClassName by your table object class name.
/** * Stores the object in the database while setting default culture if necessary. * * If the object is new, it inserts it; otherwise an update is performed. * All related objects are also updated in this method. * * @param Connection $con The database connection * @return int The number of rows affected by this insert/update and any referring fk objects' save() operations. * @throws PropelException Any exceptions caught during processing will be * rethrown wrapped into a PropelException. * @see save() */ protected function doSave($con) { $default_culture = sfConfig::get('sf_i18n_default_culture'); // We try to fetch the default culture translation to initialise the new culture. if (!isset($this->current_i18n[$default_culture])) { $obj = TableClassNameI18nPeer::retrieveByPK($this->getId(), $default_culture, $con); if ($obj) // Test if there is a translation for current culture { $this->setTableClassNameI18nForCulture($obj, $default_culture); } } else { $obj = $this->current_i18n[$default_culture]; } if(!$obj && isset($this->current_i18n[$this->culture])) { $new_i18n = new TableClassNameI18n(); $this->current_i18n[$this->culture]->copyInto($new_i18n); $new_i18n->setId($this->getId()); $new_i18n->setCulture($default_culture); $this->setTableClassNameI18nForCulture($new_i18n, $default_culture); } return parent::doSave($con); }
To complete this snippet, here is a fallback version of doSelectWithI18n.
To enable the fallback, edit your table object peer class in lib/model/TableClassNamePeer.php and add the following code. Then search and replace TableClassName by your table object class name.
/** * Selects a collection of TableClassName objects pre-filled with their i18n objects. * * @param Criteria $criteria * @param string $culture The selected culture. * @param Connection $con An optional database connection * @return array Array of TableClassName objects. * @throws PropelException Any exceptions caught during processing will be * rethrown wrapped into a PropelException. */ public static function doSelectWithI18n(Criteria $c, $culture = null, $con = null) { if ($culture === null) { $culture = sfContext::getInstance()->getUser()->getCulture(); } $default_culture = sfConfig::get('sf_i18n_default_culture'); // Set the correct dbName if it has not been overridden if ($c->getDbName() == Propel::getDefaultDB()) { $c->setDbName(self::DATABASE_NAME); } TableClassNamePeer::addSelectColumns($c); $startcol = (TableClassNamePeer::NUM_COLUMNS - TableClassNamePeer::NUM_LAZY_LOAD_COLUMNS) + 1; TableClassNameI18nPeer::addSelectColumns($c); $c->addJoin(TableClassNamePeer::ID, TableClassNameI18nPeer::ID); $criterion = $c->getNewCriterion(TableClassNameI18nPeer::CULTURE, $culture); $criterion->addOr($c->getNewCriterion(TableClassNameI18nPeer::CULTURE, $default_culture)); $c->add($criterion); $rs = BasePeer::doSelect($c, $con); $results = array(); $uncultured_results = array(); while($rs->next()) { $omClass = TableClassNamePeer::getOMClass(); $cls = Propel::import($omClass); $obj1 = new $cls(); $obj1->hydrate($rs); $obj1->setCulture($culture); if(isset($results[$obj1->getId()])) { $obj1 = $results[$obj1->getId()]; } $omClass = TableClassNameI18nPeer::getOMClass($rs, $startcol); $cls = Propel::import($omClass); $obj2 = new $cls(); $obj2->hydrate($rs, $startcol); $obj1->setTableClassNameI18nForCulture($obj2, $obj2->getCulture()); $obj2->setTableClassName($obj1); if(!isset($uncultured_results[$obj1->getId()])) { $uncultured_results[$obj1->getId()] = $obj1; } if($obj2->getCulture() == $culture) { $uncultured_results[$obj1->getId()] = false; } if(!isset($results[$obj1->getId()])) { $results[$obj1->getId()] = $obj1; } elseif($obj2->getCulture() == $culture) { // Move result to the end of results array to fit eventual sort // criteria (ugly fix). unset($results[$obj1->getId()]); $results[$obj1->getId()] = $obj1; } } foreach($uncultured_results as $obj1) { if($obj1) { $obj1->setCulture($default_culture); $default_culture_object = $obj1->getCurrentTableClassNameI18n(); if($default_culture_object) { $obj2 = new TableClassNameI18n(); $default_culture_object->copyInto($obj2); $obj2->setCulture($culture); $obj2->setTableClassName($obj1); $obj1->setTableClassNameI18nForCulture($obj2, $obj2->getCulture()); } $obj1->setCulture($culture); } } return array_values($results); }
If you want to use a pager with a filter on translations, you will need this count method. Once this method present, the magic is done by :
$criteria->setDistinct(); $pager->setCriteria($criteria); $pager->setPeerMethod('doSelectWithI18n'); $pager->setPeerCountMethod('doCountWithI18n');
Note : The setDistinct is very important for this snippet to work. It should not falsify your results, and without it, the doCountWithI18n method could return bad results.
To enable the fallback, edit your table object peer class in lib/model/TableClassNamePeer.php and add the following code. Then search and replace TableClassName by your table object class name.
/** * Returns the number of rows matching criteria with I18N criteria. * * @param Criteria $criteria * @param boolean $distinct Whether to select only distinct columns (You can also set DISTINCT modifier in Criteria). * @param Connection $con An optional database connection * @param string $culture The selected culture. * @return int Number of matching rows. */ public static function doCountWithI18n(Criteria $criteria, $distinct = false, $con = null, $culture = null) { // we're going to modify criteria, so copy it first $criteria = clone $criteria; $default_culture = sfConfig::get('sf_i18n_default_culture'); if ($culture === null) { // We use current user culture. $culture = sfContext::getInstance()->getUser()->getCulture(); } // clear out anything that might confuse the ORDER BY clause $criteria->clearSelectColumns()->clearOrderByColumns(); $criteria->addSelectColumn(TableClassNamePeer::COUNT_DISTINCT); // just in case we're grouping: add those columns to the select statement foreach($criteria->getGroupByColumns() as $column) { $criteria->addSelectColumn($column); } $criteria->addJoin(TableClassNamePeer::ID, TableClassNameI18nPeer::ID); $criterion = $criteria->getNewCriterion(TableClassNameI18nPeer::CULTURE, $culture); $criterion->addOr($criteria->getNewCriterion(TableClassNameI18nPeer::CULTURE, $default_culture)); $criteria->add($criterion); $rs = TableClassNamePeer::doSelectRS($criteria, $con); if ($rs->next()) { return $rs->getInt(1); } else { // no rows returned; we infer that means 0 matches. return 0; } }
$sql = 'select * from ( select * from book order by weight desc limit 5 ) as T order by popularity'; $connection = Propel::getConnection(); $statement = $connection->createStatement( ); $result = $statement->executeQuery( $sql , ResultSet::FETCHMODE_NUM); return BookPeer::populateObjects( $result );
This is a version of sfMemcached modified to allow multiple servers to be used. Servers are grouped in buckets for organization.
A bucket must be initialized before it is used.
Example:
sfAdvMemcachedCache::createInstance('function', array ( 'lifetime' => 60 'servers' => array ( array ( 'host' => '72.29.86.195', 'port' => 11211, 'persistent' => true, 'weight' => 2, 'timeout' => 1, 'retry_interval' => 1, 'status' => 1, ) ) ));
Then the bucket can be used through a call to getInstnace(bucketName)
Ex:
sfAdvMemcacheCache::getInstance('function')->set('helloKey', 'world_data');
For ease of use, multiple buckets and servers can be defined and auto-intialized through a YAML file called memcached.yml that will sit in the /config folder of your application.
Example configuration:
default: servers: default: host: localhost port: 11211 timeout: 1 weight: 1 retry_interval: 1 status: 1 host1: host: 72.29.86.195 port: 11211 weight: 2 host2: host: 66.7.208.83 port: 11211 weight: 2 host3: host: 66.7.208.87 port: 11211 buckets: default: lifetime: 60 servers: host1 session: servers: [host1, host2] object: servers: [host1] function: servers: host2 view:
In order to use YAML configuration a file named "myMemcacheConfigHandler.class.php" must be created in the /lib folder of your application.
<?php class myMemcacheConfigHandler extends sfYamlConfigHandler { /** * Executes this configuration handler. * * @param array An array of absolute filesystem path to a configuration file * * @return string Data to be written to a cache file * * @throws <b>sfParseException</b> If a requested configuration file is improperly formatted */ public function execute($configFiles) { // parse the yaml $myConfig = $this->parseYamls($configFiles); $myConfig = sfToolkit::arrayDeepMerge( isset($myConfig['default']) && is_array($myConfig['default']) ? $myConfig['default'] : array(), isset($myConfig['all']) && is_array($myConfig['all']) ? $myConfig['all'] : array(), isset($myConfig[sfConfig::get('sf_environment')]) && is_array($myConfig[sfConfig::get('sf_environment')]) ? $myConfig[sfConfig::get('sf_environment')] : array() ); // Set Up Servers $defaultServerConfig = array('host' => 'localhost', 'port' => 11211, 'persistent' => true, 'weight' => 1, 'timeout' => 1, 'retry_interval' => 15, 'status' => true, ); if(!isset($myConfig['servers']) || !is_array($myConfig['servers'])) { $myConfig['servers'] = array( 'default' => $defaultServerConfig ); } $myConfig['servers']['default'] = array_merge( $defaultServerConfig, isset($myConfig['servers']['default']) ? $myConfig['servers']['default'] : array() ); foreach($myConfig['servers'] as $serverName => &$server) { $server = array_merge($myConfig['servers']['default'], $server); } // Set Up Buckets if(!isset($myConfig['buckets']) || !is_array($myConfig['buckets'])) throw new sfParseException(sprintf('Configuration file "%s" does not specify any cache buckets.', $configFiles[0])); $inits = array(); foreach($myConfig['buckets'] as $bucketName => &$bucket) { if(!isset($bucket['servers'])) { $bucket['servers'] = 'default'; } if(!is_array($bucket['servers'])) { $bucket['servers'] = array( $bucket['servers'] ); } foreach($bucket['servers'] as $serverName => &$server) { if(!isset($myConfig['servers'][$server])) throw new sfParseException(sprintf('Configuration file "%s" requires server configuration \'%s\' for bucket \'%s\', but server configuration does not exist.', $configFiles[0], $server, $bucketName )); $server = $myConfig['servers'][$server]; } $inits[] = sprintf("sfAdvMemcachedCache::createInstance('%s', %s);", $bucketName, var_export($bucket, true) ); } // Compile Return Value return sprintf("<?php\n %s", implode("\n", $inits)); } }
Once that code is in place, modify your config.php in your applications /config directory.
Add the following code:
include(sfConfigCache::getInstance()->checkConfig(sfConfig::get('sf_app_config_dir_name').'/memcache.yml'));
Lastly create a file called sfAdvMemcachedCache.php in your /lib folder. All of the above is optional, and is only needed if you wanted to use YAML for auto-initialization.
... it seems there was too much code in here for a snippet. Trying to save the snippet would cause a silent failure. You can download the code here:
http://lu.scio.us/files/sfAdvMemcachedCache.php.text
Using this code as a base, it is trivial to create a Memcached class to handle session data, cache view content, or cache propel objects automatically.
The first two classes I have already written, and may be released if I repackage this code into a plugin.
If you're like me, you hate having to populate your objects (from the model) with parameters from the request. Wouldn't it be nice to specify a little nugget of configuration, and then never worry about it again? Consider the following, which is tedious and boring:
class mymoduleActions extends sfActions { ... public function executeUpdate() { $employee = new Employee(); $employee->setFirstName($this->getRequestParameter('first_name')); $employee->setLastName($this->getRequestParameter('last_name')); $employee->setSsn($this->getRequestParameter('ssn')); $employee->setDob($this->getRequestParameter('dob')); ... $this->employee = $employee; } }
And the list goes on. Imagine, however, that you have the following in your module filters.yml file:
myPopulateObjectFilter: class: myPopulateObjectFilter param: use_database: on model_object: employee model_class: Employee exclude: [ssn] defaults: first_name: example default name
If you had a filter that automatically created an object, and based upon the previous yaml, populated that object with data from the request (after a user hit submit on a form containing this information), then subsequently set that object for retrieval as a request attribute, you'd be a pretty happy person right?
class mymoduleActions extends sfActions { ... public function executeUpdate() { $this->employee = $this->getRequest()->getAttribute('employee'); ... } }
Of course, you'd be free to do whatever you want with the results of the prepopulated object.
A great application would be an alternative to the fillin form filter. If you have your template set up to populate inputs from an object from the model, this filter can populate that object for you, and you can simply pass it on to the template.
<?php /** * myPopulateObjectFilter * Uses configuration in filters.yml to create an object, * populate it with data from the request, * and set a request attribute with the result. * * @author Stephen Riesenberg */ class myPopulateObjectFilter extends sfFilter { protected $defaults = null, $controller = null, $request = null; public function initialize($context, $parameters = array()) { parent::initialize($context, $parameters); // get defaults from filters.yml (if any) and create new parameter holder to store them $this->defaults = new sfParameterHolder(); $this->defaults->add($this->getParameter('defaults', array())); // get controller and request $this->controller = $this->getContext()->getController(); $this->request = $this->getContext()->getRequest(); } public function execute($filterChain) { // get request variable list $vars = $this->request->getParameterHolder()->getNames(); $exclude = array_merge($this->getParameter('exclude', array()), array('module', 'action')); $vars = array_diff($vars, $exclude); $funcs = array(); foreach ($vars as $var) { $funcs[$var] = 'set'.ucfirst(sfInflector::camelize($var)); } // get model_object and model_class $object = $this->getParameter('model_object'); $class = $this->getParameter('model_class'); // fetch from the database or create the object to fill in if ($this->getParameter('use_database', false) && null !== ($id = $this->request->getParameter('id'))) { $peer_class = $class . "Peer"; $record = $peer_class::retrieveByPk($id); } else { $record = new $class(); } // something like: array('id' => 'setId', 'my_field' => 'setMyField', ...) foreach ($funcs as $var => $func) { if (is_callable(array($record, $func))) { $record->$func($this->getValue($var)); } } // set the updated record into a request attribute $this->request->setAttribute($object, $record); // execute next filter $filterChain->execute(); } protected function getDefault($var) { return $this->defaults->get($var); } protected function getValue($var) { return $this->request->getParameter($var) != '' ? $this->request->getParameter($var) : $this->getDefault($var); } }
NOTE: Place this in a file called myPopulateObjectFilter.class.php in the myproject/apps/myapp/lib/ directory.
function getxmlhttp() { //Create a boolean variable to check for a valid IE instance. var xmlhttp = false; //Check if we are using IE. try { //If the javascript version is greater than 5. xmlhttp = new ActiveXObject("Msxml2.XMLHTTP"); } catch (e) { //If not, then use the older active x object. try { //If we are using IE. xmlhttp = new ActiveXObject("Microsoft.XMLHTTP"); } catch (E) { //Else we must be using a non-IE browser. xmlhttp = false; } } //If we are using a non-IE browser, create a JavaScript instance of the object. if (!xmlhttp && typeof XMLHttpRequest != 'undefined') { xmlhttp = new XMLHttpRequest(); } return xmlhttp; }
function getxmlhttp() { //Create a boolean variable to check for a valid IE instance. var xmlhttp = false; //Check if we are using IE. try { //If the javascript version is greater than 5. xmlhttp = new ActiveXObject("Msxml2.XMLHTTP"); } catch (e) { //If not, then use the older active x object. try { //If we are using IE. xmlhttp = new ActiveXObject("Microsoft.XMLHTTP"); } catch (E) { //Else we must be using a non-IE browser. xmlhttp = false; } } //If we are using a non-IE browser, create a JavaScript instance of the object. if (!xmlhttp && typeof XMLHttpRequest != 'undefined') { xmlhttp = new XMLHttpRequest(); } return xmlhttp; } [/code/
This in the easiest way (I found) to debug an object (or variable).
$this->debugMessage(sprintf('<h1>object</h1><pre>%s</pre>', print_r(@$this->object, true)));
<?php use_helper('Debug') ?> <?php echo debug_message(sprintf('<h1>object</h1><pre>%s</pre>', print_r(@$object, true))) ?>
Based on symfony book debug page.
my approach to the previous snippet is to create a class called debugTools.class.php and throw it in the \lib directory. It should contain the following code:
<? Class debugTools { /** * renders to the page a proper object map for looking over contents of an object as it occurs. * * @param object $object (required) the object for inpection */ Public Static function inspect($object) { echo "<PRE>"; print_r($object); echo "</PRE>"; } }// ends class ?>
you can then call it easily (with only two or three keystrokes if your IDE does autocompletion) by calling:
echo debugTools::inspect($object);
the resulting formating looks like so (i used it to analyze a usp shipping object):
ups Object ( [_errors] => Array ( ) [_action] => 3 [_delivery_code] => GND [_src_country] => US [_dst_country] => US [_rate_chart] => 5 [_container] => 0 [_rescom] => 1 [_rate_charts] => Array ( [0] => Regular+Daily+Pickup [1] => On+Call+Air [2] => One+Time+Pickup [3] => Letter+Center [4] => Customer+Counter [5] => Drop+Off ) [_containers] => Array ( [0] => 00 [1] => 01 [2] => 21 [3] => 22 [4] => 23 [5] => 1 [6] => 2 ) [_rescoms] => Array ( [0] => 1 [1] => 2 ) [_src_zip] => 97499 [_dst_zip] => 90120 [_weight] => 2 )
a handy way of viewing objects that can be accessed from the layout or pretty much anywhere, if you're new to symfony use it to look over $sf_user in the template, you will have a much better idea what's going on.