diff --git a/README.markdown b/README.markdown index 5b3964d..b6f8dca 100644 --- a/README.markdown +++ b/README.markdown @@ -22,6 +22,8 @@ for data exchange. Here are some key features : ## How to install +### Symfony automatic install + * go to your project's root * Install the plugin: @@ -33,8 +35,13 @@ for data exchange. Here are some key features : ./symfony cc +### Using subversion (the code might be older!) + + * [http://svn.symfony-project.com/plugins/sfDoctrineRestGeneratorPlugin](http://svn.symfony-project.com/plugins/sfDoctrineRestGeneratorPlugin). + +#### Using git - * alternatively, you might prefer to install this plugin as a Subversion dependancy. In this case, here is the repository: [http://svn.symfony-project.com/plugins/sfDoctrineRestGeneratorPlugin](http://svn.symfony-project.com/plugins/sfDoctrineRestGeneratorPlugin). There is also a Git mirror on GitHub : [https://github.com/xavierlacot/sfDoctrineRestGeneratorPlugin](https://github.com/xavierlacot/sfDoctrineRestGeneratorPlugin) + * [https://github.com/xavierlacot/sfDoctrineRestGeneratorPlugin](https://github.com/xavierlacot/sfDoctrineRestGeneratorPlugin) ## Usage @@ -78,7 +85,7 @@ If we want to expose the model "Post" through a REST API, we will simply type the command: - ./symfony doctrine:generate-rest-module api post Post + ./symfony doctrine:generate-rest-module api post Post This will generate: @@ -100,6 +107,8 @@ This will generate: * lib: contains an empty "postGeneratorConfiguration" class, which extends a on-the-fly generated "BasePostGeneratorConfiguration" class, * templates + * a lime functional test skeleton in /test/functional/api + * after a first request has been made to the REST module, the cache directory will contain the code of the generated module, and particularly the code of the "autopostActions" class, which you should check in order to understand the way the plugin works. @@ -121,7 +130,6 @@ http://api.example.com/myPostAPI.json: format: xml - ## Main configuration Before configuring the content of the response of the webservice, the first @@ -161,7 +169,8 @@ Second, consider a way to make your webservice more secure: * use SSL whenever possible, so that the posted data do not get intercepted and altered by a third party (man in the middle), * use HTTP authentication, * use a stronger / more extensible authentication system (OAuth for example), - * deliver unique API keys to your clients, and check the usage that they do of the API. + * deliver unique API keys to your clients, and check the usage that they do of the API, + * improve the generated validators with your own business rules ## Detailed service configuration @@ -175,7 +184,7 @@ Here is the default content of the `generator.yml` file: class: sfDoctrineRestGenerator param: model_class: Post - + # actions_base_class: sfActions config: default: # fields: # list here the fields. @@ -183,6 +192,9 @@ Here is the default content of the `generator.yml` file: # formats_enabled: [ json, xml, yaml ] # enabled formats # formats_strict: true # separator: ',' # separator used for multiple filters + # camelize: true # tell the serializers to translate fieldnames to camelCase + # root_name: ##MODEL_CLASS## # the root node name used for serilize one ressource + # plural_root_name: false # you can overide the plural root node name (if false, we add a "s" to root_name) get: # additional_params: [] # list here additional params names, which are not object properties # default_format: json # the default format of the response. If not set, will default to json. Accepted values are "json", "xml" or "yaml" @@ -211,6 +223,11 @@ detailed in the following chapters. The `model_class` parameters defines the name of the Doctrine model the REST module is bound to. +### actions_base_class + +All the generated controllers will extend this class. + + ### default The `default` option contains several general configuration directives: @@ -256,12 +273,27 @@ The separator to use in url when passing objects primary keys. The generated module allows to require several resources identified by their ids: http://api.example.com/post/?id=12,17,19 + # camelize: true # tell the serializers to translate fieldnames to camelCase + # root_name: ##MODEL_CLASS## # the root node name used for serilize one ressource + # plural_root_name: false # you can overide the plural root node name (if false, we add a "s" to root_name) + +### camelize + +On serialization, your fieldnames will be camelCased by default (only XML atm). + +### root_name + +You can customize the root_name of the Json / Xml outputs (by default, it's the model name). + +### plural_root_name + +By default, collections of ressources are put in a "s" suffixed model name. But for a Company model, you don't want to have a `Companys` XML root name. Simply write your own here. + ### get The `get` option lists several options specific to the "get" operation: - #### additional_params The `default_format` option allows to define an array of parameter names, @@ -591,11 +623,12 @@ picking one of the following topics: * possibility to disable events notification / filtering (performance) * more serializers ([BSON](http://bsonspec.org/) or RDF for example). Currently, the plugin only allows to serialize the resultsets as a XML, YAML or JSON feeds (see the chapter "Serialization"). Mobile clients, which require the most compact possible streams, would take benefit from a BSON serialization. * possibility to generate client libraries (sfDoctrineRestClientGenerator ?) - * possibility to generate unit tests * possibility to generate API documentation * document authentication solutions * all the possible feedback! - + * recode the post validators logic (to use the full params array on each post validator, same as sfForm) + * camelize on all the serializer? (only XML do that atm) + * don't use the templating system (performance) ## Contribute to the plugin, ask for help @@ -611,10 +644,20 @@ licensed under the MIT license. ## Changelog -### trunk +### master on damienalexandre/sfDoctrineRestGeneratorPlugin - * Enabled the `show` route by default + * Improve the XML and Json serializer (deal with collections, plural naming, Json and XML are now a lot more consistant) + * Add a `camelize` option + * Add a `root_name` option + * Add a `plural_root_name` option + * Use the query() method in update and delete action (you can now have consistent query on all methods) + * The validated payload array is now kept and send to updateObjectFromRequest(). This is allowing a lot of flexibility (like saving sfValidatedFile instance i.e.) + * Add a dynamically generated functional test on each new module + * Add a `location` header on create and update response with the uri to the new ressource +### master + + * Enabled the `show` route by default ### version 0.9.4 - 2010-11-25 diff --git a/data/generator/sfDoctrineRestGenerator/default/parts/configuration.php b/data/generator/sfDoctrineRestGenerator/default/parts/configuration.php index 514ceee..d367465 100644 --- a/data/generator/sfDoctrineRestGenerator/default/parts/configuration.php +++ b/data/generator/sfDoctrineRestGenerator/default/parts/configuration.php @@ -3,10 +3,7 @@ /** * getModuleName() ?> module configuration. * - * @package ##PROJECT_NAME## * @subpackage getModuleName()."\n" ?> - * @author ##AUTHOR_NAME## - * @version SVN: $Id: configuration.php 24171 2009-11-19 16:37:50Z Kris.Wallsmith $ */ abstract class BasegetModuleName()) ?>GeneratorConfiguration extends sfDoctrineRestGeneratorConfiguration { @@ -91,6 +88,24 @@ public function getSeparator() config['default']['separator']) ?> } + public function getCamelize() + { + return asPhp(isset($this->config['default']['camelize']) ? $this->config['default']['camelize'] : true) ?>; +config['default']['camelize']) ?> + } + + public function getRootName() + { + return asPhp(isset($this->config['default']['root_name']) ? $this->config['default']['root_name'] : $this->getModelClass()) ?>; +config['default']['root_name']) ?> + } + + public function getPluralRootName() + { + return asPhp(isset($this->config['default']['plural_root_name']) ? $this->config['default']['plural_root_name'] : false) ?>; +config['default']['plural_root_name']) ?> + } + diff --git a/data/generator/sfDoctrineRestGenerator/default/parts/createAction.php b/data/generator/sfDoctrineRestGenerator/default/parts/createAction.php index c6d94f5..c8a9fb9 100644 --- a/data/generator/sfDoctrineRestGenerator/default/parts/createAction.php +++ b/data/generator/sfDoctrineRestGenerator/default/parts/createAction.php @@ -22,7 +22,8 @@ public function executeCreate(sfWebRequest $request) try { - $this->validateCreate($content); + $params = $this->parsePayload($content); + $params = $this->validateCreate($params); } catch (Exception $e) { @@ -52,6 +53,6 @@ public function executeCreate(sfWebRequest $request) } $this->object = $this->createObject(); - $this->updateObjectFromRequest($content); + $this->updateObjectFromRequest($params); return $this->doSave(); } diff --git a/data/generator/sfDoctrineRestGenerator/default/parts/deleteAction.php b/data/generator/sfDoctrineRestGenerator/default/parts/deleteAction.php index d3de9ea..d2b66ec 100644 --- a/data/generator/sfDoctrineRestGenerator/default/parts/deleteAction.php +++ b/data/generator/sfDoctrineRestGenerator/default/parts/deleteAction.php @@ -10,7 +10,7 @@ public function executeDelete(sfWebRequest $request) $this->forward404Unless($request->isMethod(sfRequest::DELETE)); $primaryKey = $request->getParameter(''); $this->forward404Unless($primaryKey); - $this->item = Doctrine::getTable($this->model)->findOneBy($primaryKey); + $this->item = $this->query(array('' => $primaryKey))->fetchOne(); $this->forward404Unless($this->item); $this->item->delete(); return sfView::NONE; diff --git a/data/generator/sfDoctrineRestGenerator/default/parts/doSave.php b/data/generator/sfDoctrineRestGenerator/default/parts/doSave.php index 38a1b5d..14d793a 100644 --- a/data/generator/sfDoctrineRestGenerator/default/parts/doSave.php +++ b/data/generator/sfDoctrineRestGenerator/default/parts/doSave.php @@ -2,6 +2,7 @@ protected function doSave() { $this->object->save(); +configuration->getValue('default.update_key', Doctrine::getTable($this->getModelClass())->getIdentifier()); ?> // Set a Location header with the path to the new / updated object $this->getResponse()->setHttpHeader('Location', $this->getController()->genUrl( array_merge(array( diff --git a/data/generator/sfDoctrineRestGenerator/default/parts/getSerializer.php b/data/generator/sfDoctrineRestGenerator/default/parts/getSerializer.php index b68d95b..ca05915 100644 --- a/data/generator/sfDoctrineRestGenerator/default/parts/getSerializer.php +++ b/data/generator/sfDoctrineRestGenerator/default/parts/getSerializer.php @@ -4,11 +4,11 @@ protected function getSerializer() { try { - $this->serializer = sfResourceSerializer::getInstance($this->getFormat()); + $this->serializer = sfResourceSerializer::getInstance($this->getFormat(), asPhp($this->configuration->getValue('default.camelize')) ?>); } catch (sfException $e) { - $this->serializer = sfResourceSerializer::getInstance('configuration->getValue('get.default_format') ?>'); + $this->serializer = sfResourceSerializer::getInstance('configuration->getValue('get.default_format') ?>', asPhp($this->configuration->getValue('default.camelize')) ?>); throw new sfException($e->getMessage()); } } diff --git a/data/generator/sfDoctrineRestGenerator/default/parts/indexAction.php b/data/generator/sfDoctrineRestGenerator/default/parts/indexAction.php index 8f04b47..b36301d 100644 --- a/data/generator/sfDoctrineRestGenerator/default/parts/indexAction.php +++ b/data/generator/sfDoctrineRestGenerator/default/parts/indexAction.php @@ -17,7 +17,7 @@ public function executeIndex(sfWebRequest $request) try { $format = $this->getFormat(); - $this->validateIndex($params); + $params = $this->validateIndex($params); } catch (Exception $e) { @@ -82,6 +82,6 @@ public function executeIndex(sfWebRequest $request) $serializer = $this->getSerializer(); $this->getResponse()->setContentType($serializer->getContentType()); - $this->output = $serializer->serialize($this->objects, $this->model); + $this->output = $serializer->serialize($this->objects, asPhp($this->configuration->getValue('default.root_name')); ?>, true, asPhp($this->configuration->getValue('default.plural_root_name')); ?>); unset($this->objects); } diff --git a/data/generator/sfDoctrineRestGenerator/default/parts/parsePayload.php b/data/generator/sfDoctrineRestGenerator/default/parts/parsePayload.php index dcbb0a9..474399c 100644 --- a/data/generator/sfDoctrineRestGenerator/default/parts/parsePayload.php +++ b/data/generator/sfDoctrineRestGenerator/default/parts/parsePayload.php @@ -10,7 +10,7 @@ protected function parsePayload($payload, $force = false) $payload_array = $serializer->unserialize($payload); } - if (!isset($payload_array) || !$payload_array) + if (!isset($payload_array) || $payload_array === false) { throw new sfException(sprintf('Could not parse payload, obviously not a valid %s data!', $format)); } diff --git a/data/generator/sfDoctrineRestGenerator/default/parts/showAction.php b/data/generator/sfDoctrineRestGenerator/default/parts/showAction.php index 935934f..0eaca23 100644 --- a/data/generator/sfDoctrineRestGenerator/default/parts/showAction.php +++ b/data/generator/sfDoctrineRestGenerator/default/parts/showAction.php @@ -47,7 +47,7 @@ public function executeShow(sfWebRequest $request) } $this->queryFetchOne($params); - $this->forward404Unless(is_array($this->objects[0])); + $this->forward404Unless(is_array($this->objects[0]) && !empty($this->objects[0])); configuration->getValue('get.object_additional_fields') as $field): ?> $this->embedAdditional(0, $params); @@ -61,6 +61,6 @@ public function executeShow(sfWebRequest $request) $serializer = $this->getSerializer(); $this->getResponse()->setContentType($serializer->getContentType()); - $this->output = $serializer->serialize($this->objects[0], $this->model, false); + $this->output = $serializer->serialize($this->objects[0], asPhp($this->configuration->getValue('default.root_name')); ?>, false); unset($this->objects); } diff --git a/data/generator/sfDoctrineRestGenerator/default/parts/updateAction.php b/data/generator/sfDoctrineRestGenerator/default/parts/updateAction.php index 3438a33..5bcb306 100644 --- a/data/generator/sfDoctrineRestGenerator/default/parts/updateAction.php +++ b/data/generator/sfDoctrineRestGenerator/default/parts/updateAction.php @@ -16,9 +16,16 @@ public function executeUpdate(sfWebRequest $request) $request->setRequestFormat('html'); + // retrieve the object +getModelClass())->getIdentifier() ?> + $primaryKey = $request->getParameter(''); + $this->object = $this->query(array('' => $primaryKey))->fetchOne(); + $this->forward404Unless($this->object); + try { - $this->validateUpdate($content); + $params = $this->parsePayload($content); + $params = $this->validateUpdate($params); } catch (Exception $e) { @@ -47,13 +54,7 @@ public function executeUpdate(sfWebRequest $request) return sfView::SUCCESS; } - // retrieve the object -getModelClass())->getIdentifier() ?> - $primaryKey = $request->getParameter(''); - $this->object = Doctrine_Core::getTable($this->model)->findOneBy($primaryKey); - $this->forward404Unless($this->object); - // update and save it - $this->updateObjectFromRequest($content); + $this->updateObjectFromRequest($params); return $this->doSave(); } diff --git a/data/generator/sfDoctrineRestGenerator/default/parts/updateObjectFromRequest.php b/data/generator/sfDoctrineRestGenerator/default/parts/updateObjectFromRequest.php index e60137b..51a4f9d 100644 --- a/data/generator/sfDoctrineRestGenerator/default/parts/updateObjectFromRequest.php +++ b/data/generator/sfDoctrineRestGenerator/default/parts/updateObjectFromRequest.php @@ -1,4 +1,4 @@ - protected function updateObjectFromRequest($content) + protected function updateObjectFromRequest($params) { - $this->object->importFrom('array', $this->parsePayload($content)); + $this->object->importFrom('array', $params); } diff --git a/data/generator/sfDoctrineRestGenerator/default/parts/validate.php b/data/generator/sfDoctrineRestGenerator/default/parts/validate.php index 0c92b20..4e9de32 100644 --- a/data/generator/sfDoctrineRestGenerator/default/parts/validate.php +++ b/data/generator/sfDoctrineRestGenerator/default/parts/validate.php @@ -1,5 +1,6 @@ /** * Applies a set of validators to an array of parameters + * The cleaned value replace * * @param array $params An array of parameters * @param array $validators An array of validators @@ -7,6 +8,12 @@ */ public function validate($params, $validators, $prefix = '') { + if ($params === null && is_array($validators)) + { + // The case of an empty collection + return null; + } + $unused = array_keys($validators); foreach ($params as $name => $value) @@ -19,12 +26,31 @@ public function validate($params, $validators, $prefix = '') { if (is_array($validators[$name])) { - // validator for a related object - $this->validate($value, $validators[$name], $prefix.$name.'.'); + if (is_array($value) && isset($value[0])) + { + // We are on a list of array, not a related object + foreach ($value as $key => $val) + { + $params[$name][$key] = $this->validate($val, $validators[$name], $prefix.$name.'.'); + } + } + else + { + // validator for a related object + $params[$name] = $this->validate($value, $validators[$name], $prefix.$name.'.'); + } + } + elseif (is_array($value) && isset($value[0]) && $validators[$name] instanceof sfValidatorBase) + { + // We are on a list of value + foreach ($value as $key => $val) + { + $params[$name][$key] = $validators[$name]->clean($val); + } } else { - $validators[$name]->clean($value); + $params[$name] = $validators[$name]->clean($value); } unset($unused[array_search($name, $unused, true)]); @@ -46,4 +72,6 @@ public function validate($params, $validators, $prefix = '') throw new sfException(sprintf('Could not validate field "%s": %s', $prefix.$name, $e->getMessage())); } } + + return $params; } \ No newline at end of file diff --git a/data/generator/sfDoctrineRestGenerator/default/parts/validateCreate.php b/data/generator/sfDoctrineRestGenerator/default/parts/validateCreate.php index 14c39db..738fca3 100644 --- a/data/generator/sfDoctrineRestGenerator/default/parts/validateCreate.php +++ b/data/generator/sfDoctrineRestGenerator/default/parts/validateCreate.php @@ -1,15 +1,16 @@ /** * Applies the creation validators to the payload posted to the service * - * @param string $payload A payload string + * @param array $params A parsed payload array + * @return array $params A cleaned params array */ - public function validateCreate($payload) + public function validateCreate($params) { - $params = $this->parsePayload($payload); - $validators = $this->getCreateValidators(); - $this->validate($params, $validators); + $params = $this->validate($params, $validators); $postvalidators = $this->getCreatePostValidators(); $this->postValidate($params, $postvalidators); + + return $params; } diff --git a/data/generator/sfDoctrineRestGenerator/default/parts/validateIndex.php b/data/generator/sfDoctrineRestGenerator/default/parts/validateIndex.php index 2dd85ec..a4d152a 100644 --- a/data/generator/sfDoctrineRestGenerator/default/parts/validateIndex.php +++ b/data/generator/sfDoctrineRestGenerator/default/parts/validateIndex.php @@ -3,12 +3,15 @@ * webservice * * @param array $params An array of criterions used for the selection + * @return array A cleaned array of criterions */ public function validateIndex($params) { $validators = $this->getIndexValidators(); - $this->validate($params, $validators); + $params = $this->validate($params, $validators); $postvalidators = $this->getIndexPostValidators(); $this->postValidate($params, $postvalidators); + + return $params; } diff --git a/data/generator/sfDoctrineRestGenerator/default/parts/validateUpdate.php b/data/generator/sfDoctrineRestGenerator/default/parts/validateUpdate.php index 1f96557..6bac9d1 100644 --- a/data/generator/sfDoctrineRestGenerator/default/parts/validateUpdate.php +++ b/data/generator/sfDoctrineRestGenerator/default/parts/validateUpdate.php @@ -1,15 +1,16 @@ /** * Applies the update validators to the payload posted to the service * - * @param string $payload A payload string + * @param array $params A parsed payload array + * @return array $params A cleaned params array */ - public function validateUpdate($payload) + public function validateUpdate($params) { - $params = $this->parsePayload($payload); - $validators = $this->getUpdateValidators(); - $this->validate($params, $validators); + $params = $this->validate($params, $validators); $postvalidators = $this->getUpdatePostValidators(); $this->postValidate($params, $postvalidators); + + return $params; } diff --git a/data/generator/sfDoctrineRestGenerator/default/skeleton/config/generator.yml b/data/generator/sfDoctrineRestGenerator/default/skeleton/config/generator.yml index 9a437f1..ec96e2f 100644 --- a/data/generator/sfDoctrineRestGenerator/default/skeleton/config/generator.yml +++ b/data/generator/sfDoctrineRestGenerator/default/skeleton/config/generator.yml @@ -2,7 +2,7 @@ generator: class: sfDoctrineRestGenerator param: model_class: ##MODEL_CLASS## - +# actions_base_class: sfActions config: default: # fields: # list here the fields. @@ -10,6 +10,9 @@ generator: # formats_enabled: [ json, xml, yaml ] # enabled formats # formats_strict: true # separator: ',' # separator used for multiple filters +# camelize: true # tell the serializers to translate fieldnames to camelCase +# root_name: ##MODEL_CLASS## # the root node name used for serilize one ressource +# plural_root_name: false # you can overide the plural root node name (if false, we add a "s" to root_name) get: # additional_params: [] # list here additionnal params names, which are not object properties # default_format: json # the default format of the response. If not set, will default to json. accepted values are "json", "xml" or "yaml" @@ -24,7 +27,7 @@ generator: # pagination_enabled: false # set to true to activate the pagination # pagination_custom_page_size: false # set to true to allow the client to pass a page_size parameter # pagination_page_size: 100 # the default number of items in a page -# sort_custom: false # set to true to allow the client to pass a sort_by and a sort_order parameter +# sort_custom: false # set to true to allow the client to pass a orderby and a sortorder parameter # sort_default: [] # set to [column, asc|desc] in order to sort on a column # filters: # list here the filters # created_at: { date_format: 'd-m-Y', multiple: true } # for instance diff --git a/lib/generator/sfDoctrineRestGeneratorConfiguration.class.php b/lib/generator/sfDoctrineRestGeneratorConfiguration.class.php index 421cc02..1da3537 100644 --- a/lib/generator/sfDoctrineRestGeneratorConfiguration.class.php +++ b/lib/generator/sfDoctrineRestGeneratorConfiguration.class.php @@ -20,7 +20,9 @@ protected function compile() 'fields' => $this->getFieldsDefault(), 'formats_enabled' => $this->getFormatsEnabled(), 'formats_strict' => $this->getFormatsStrict(), - 'separator' => $this->getSeparator() + 'separator' => $this->getSeparator(), + 'camelize' => $this->getCamelize(), + 'root_name' => $this->getRootName(), ), 'get' => array( 'additional_params' => $this->getAdditionalParams(), diff --git a/lib/serializer/sfResourceSerializer.class.php b/lib/serializer/sfResourceSerializer.class.php index 10a3411..229d2e2 100644 --- a/lib/serializer/sfResourceSerializer.class.php +++ b/lib/serializer/sfResourceSerializer.class.php @@ -2,6 +2,8 @@ abstract class sfResourceSerializer { + private $camelize = true; + abstract public function getContentType(); /** @@ -11,21 +13,39 @@ abstract public function getContentType(); * @author CakePHP * @see http://book.cakephp.org/view/572/Class-methods * @param string $string The string to camelize - * @return string with CamelCase + * @return string with CamelCase or underscored depending on configuration */ protected function camelize($string) { - return str_replace(" ", "", ucwords(str_replace("_", " ", $string))); + if ($this->camelize) + { + return str_replace(" ", "", ucwords(str_replace("_", " ", $string))); + } + else + { + return sfInflector::underscore($string); + } + } + + /** + * Tell the serializer to camelize names or to let them flat + * + * @param boolean $camelize + */ + public function setCamelize($camelize) + { + $this->camelize = $camelize; } /** - * Creates an instance of a seriazlizer + * Creates an instance of a serializer * * @param string $format The serializer format (xml, json, etc.) + * @param boolean $camelize Tell the serializer to Camelize nodes * @return object a serializer object * @throws sfException */ - public static function getInstance($format = 'xml') + public static function getInstance($format = 'xml', $camelize = true) { $classname = sprintf('sfResourceSerializer%s', ucfirst($format)); @@ -34,9 +54,11 @@ public static function getInstance($format = 'xml') throw new sfException(sprintf('Could not find seriaizer "%s"', $classname)); } - return new $classname; + $serializer = new $classname; + $serializer->setCamelize($camelize); + return $serializer; } - abstract public function serialize($array, $rootNodeName = 'data', $collection = true); + abstract public function serialize($array, $rootNodeName = 'data', $collection = true, $plural_root_name = false); abstract public function unserialize($payload); } \ No newline at end of file diff --git a/lib/serializer/sfResourceSerializerJson.class.php b/lib/serializer/sfResourceSerializerJson.class.php index cfcbf79..37d2de3 100644 --- a/lib/serializer/sfResourceSerializerJson.class.php +++ b/lib/serializer/sfResourceSerializerJson.class.php @@ -1,5 +1,9 @@ $result) + { + $array[$key] = array($rootNodeName => $result); + } + + if ($pluralRootNodeName) + { + $array = array($pluralRootNodeName => $this->pluralizeCollections($array)); + } + else + { + $array = array($rootNodeName.'s' => $this->pluralizeCollections($array)); + } + } + else + { + $array = array($rootNodeName => $this->pluralizeCollections($array)); + } + return json_encode($array); } + /** + * Mimic the XML behavior for Json serialization + * + * @param array $array + * @return array + */ + public function pluralizeCollections($array) + { + foreach ($array as $key => $nodes) + { + if (is_array($nodes) && isset($nodes[0])) + { + foreach ($nodes as $noderesult) + { + $array[$key.'s'][] = array($key => $noderesult); + } + unset($array[$key]); + } + elseif (is_array($nodes)) + { + $array[$key] = $this->pluralizeCollections($nodes); + } + } + return $array; + } + public function unserialize($payload) { - return json_decode($payload, true); + $array = json_decode($payload, true); + if ($array) + { + $array = array_shift($array); + } + + $array = $this->unPluralizeCollections($array); + + return $array; + } + + /** + * Deal with collections + * + * @param array $array + * @return array + */ + public function unPluralizeCollections($array) + { + foreach ($array as $key => $nodes) + { + if (is_array($nodes) && isset($nodes[0])) + { + $group_name = key($nodes[0]); + + foreach ($nodes as $nodekey => $noderesult) + { + unset($array[$key][$nodekey]); + $array[$key][$group_name][] = $noderesult[$group_name]; + } + } + elseif (is_array($nodes)) + { + $array[$key] = $this->unPluralizeCollections($nodes); + } + } + return $array; } } \ No newline at end of file diff --git a/lib/serializer/sfResourceSerializerXml.class.php b/lib/serializer/sfResourceSerializerXml.class.php index 10d62f1..223832b 100644 --- a/lib/serializer/sfResourceSerializerXml.class.php +++ b/lib/serializer/sfResourceSerializerXml.class.php @@ -7,15 +7,21 @@ public function getContentType() return 'application/xml'; } - public function serialize($array, $rootNodeName = 'data', $collection = true) + public function serialize($array, $rootNodeName = 'data', $collection = true, $pluralRootNodeName = false) { $camelizedRootNodeName = $this->camelize($rootNodeName); - return $this->arrayToXml($array, $camelizedRootNodeName, 0, $collection); + + if ($pluralRootNodeName) + { + $pluralRootNodeName = $this->camelize($pluralRootNodeName); + } + + return $this->arrayToXml($array, $camelizedRootNodeName, 0, $collection, $pluralRootNodeName); } /** * Transform the payload into array assuming the payload is XML formatted. - * + * * @param string $payload * @return array * @throw Exception @@ -30,6 +36,11 @@ public function unserialize($payload) throw new sfException("Empty payload, can't unserialize it."); } + // Remove all the XML comments + // Because SimpleXml use them as SimpleXmlElement and there is NO way + // to know if a node is a comment or an Element. + $payload = preg_replace('~~sm', '', $payload); + // Try to parse the XML $xml = @simplexml_load_string( $payload, @@ -42,7 +53,7 @@ public function unserialize($payload) { $errors = libxml_get_errors(); $exception_message = ''; - + foreach ($errors as $error) { $exception_message .= $this->formatXmlError($error); @@ -51,21 +62,8 @@ public function unserialize($payload) libxml_clear_errors(); throw new sfException("XML parsing error(s): \n".$exception_message); } - - $return = $this->unserializeToArray($xml); - // Shift any root node and return only the nested array - if (is_array($return) && count($return) == 1) - { - // Don't want to break up the $return array - $return_shifted = $return; - $collection_return = array_shift($return_shifted); - - if (is_array($collection_return)) - { - $return = $collection_return; - } - } + $return = $this->unserializeToArray($xml); return $return; } @@ -79,7 +77,7 @@ public function unserialize($payload) protected function formatXmlError($error) { $return = "\n\n"; - + switch ($error->level) { case LIBXML_ERR_WARNING: @@ -118,10 +116,12 @@ protected function unserializeToArray($data) if ( (!is_array($item) && (!is_object($item))) || ($item instanceof SimpleXMLElement && - (count((array) $item) < 1 || (trim((string)$item) === '') ) + (count((array) $item) < 1 || (trim((string)$item) === '') ) && + !(($tmp = (array) $item) && count($tmp) > 0 && !isset($tmp[0])) // Deep array with keys? ) ) { + $item = trim((string)$item); unset($data[$name]); @@ -129,6 +129,10 @@ protected function unserializeToArray($data) { $data[sfInflector::underscore($name)] = $this->unserializeToArray($item, true); } + else + { + $data[sfInflector::underscore($name)] = null; + } } else { @@ -140,14 +144,21 @@ protected function unserializeToArray($data) return $data; } - protected function arrayToXml($array, $rootNodeName = 'Data', $level = 0, $collection = true) + protected function arrayToXml($array, $rootNodeName = 'Data', $level = 0, $collection = true, $pluralRootNodeName = false) { $xml = ''; if (0 == $level) { - $plural = (true === $collection) ? 's' : ''; - $xml .= '<'.$rootNodeName.$plural.'>'; + if ($pluralRootNodeName) + { + $xml .= '<'.$pluralRootNodeName.'>'; + } + else + { + $plural = (true === $collection) ? 's' : ''; + $xml .= '<'.$rootNodeName.$plural.'>'; + } } foreach ($array as $key => $value) @@ -194,7 +205,14 @@ protected function arrayToXml($array, $rootNodeName = 'Data', $level = 0, $colle if (0 == $level) { - $xml .= ''; + if ($pluralRootNodeName) + { + $xml .= ''; + } + else + { + $xml .= ''; + } } return $xml; diff --git a/lib/serializer/sfResourceSerializerYaml.class.php b/lib/serializer/sfResourceSerializerYaml.class.php index c85fe0f..ca1fc58 100644 --- a/lib/serializer/sfResourceSerializerYaml.class.php +++ b/lib/serializer/sfResourceSerializerYaml.class.php @@ -7,7 +7,7 @@ public function getContentType() return 'application/yaml'; } - public function serialize($array, $rootNodeName = 'data', $collection = true) + public function serialize($array, $rootNodeName = 'data', $collection = true, $pluralRootNodeName = false) { return sfYaml::dump(array($rootNodeName => $array), 5); } diff --git a/lib/test/sfTesterJsonResponse.class.php b/lib/test/sfTesterJsonResponse.class.php index d6e9518..dd751be 100644 --- a/lib/test/sfTesterJsonResponse.class.php +++ b/lib/test/sfTesterJsonResponse.class.php @@ -20,7 +20,7 @@ public function initialize() /** * Try to decode the response body from json format - * + * * @return boolean */ public function isJson()