From 8cbd95e465b3ae8da38885b74f13c846a4c1cc2f Mon Sep 17 00:00:00 2001 From: Jamie Snape Date: Wed, 18 Jun 2014 14:57:47 -0400 Subject: [PATCH] Update Zend RESTful Framework to 7ae983a5ca --- library/REST/Controller.php | 2 +- .../Action/Helper/ContextSwitch.php | 60 +++--- .../REST/Controller/Plugin/RestHandler.php | 153 +++++++-------- library/REST/LICENSE | 35 ++-- library/REST/README.md | 174 ++++++++++++++++++ library/REST/Request.php | 46 +++++ 6 files changed, 338 insertions(+), 132 deletions(-) create mode 100644 library/REST/README.md create mode 100644 library/REST/Request.php diff --git a/library/REST/Controller.php b/library/REST/Controller.php index 6ad389d77..9a94feeea 100644 --- a/library/REST/Controller.php +++ b/library/REST/Controller.php @@ -3,7 +3,7 @@ * REST Controller default actions * */ -require_once BASE_PATH.'/core/AppController.php'; +require_once BASE_PATH . '/core/AppController.php'; abstract class REST_Controller extends AppController { diff --git a/library/REST/Controller/Action/Helper/ContextSwitch.php b/library/REST/Controller/Action/Helper/ContextSwitch.php index 57477f422..f1fe9c331 100644 --- a/library/REST/Controller/Action/Helper/ContextSwitch.php +++ b/library/REST/Controller/Action/Helper/ContextSwitch.php @@ -13,7 +13,7 @@ class REST_Controller_Action_Helper_ContextSwitch extends Zend_Controller_Action 'json' => 'Zend_Serializer_Adapter_Json', 'xml' => 'REST_Serializer_Adapter_Xml', 'php' => 'Zend_Serializer_Adapter_PhpSerialize', - 'html' => 'Zend_Serializer_Adapter_Json', + 'html' => 'Zend_Serializer_Adapter_Json' ); protected $_rest_contexts = array( @@ -74,7 +74,7 @@ class REST_Controller_Action_Helper_ContextSwitch extends Zend_Controller_Action 'options' => array( 'autoDisableLayout' => false, ), - + 'callbacks' => array( 'init' => 'initAbstractContext', 'post' => 'restContext' @@ -128,6 +128,7 @@ public function restContext() if ($view instanceof Zend_View_Interface) { if (method_exists($view, 'getVars')) { $vars = $view->getVars(); + if (isset($vars['apiresults'])) { $data = $vars['apiresults']; @@ -142,7 +143,7 @@ public function restContext() $body = str_replace('', sprintf('', $stylesheet), $body); } } - + if ($this->_currentContext == 'json') { $callback = $this->getRequest()->getParam('jsonp-callback', false); @@ -150,9 +151,9 @@ public function restContext() $body = sprintf('%s(%s)', $callback, $body); } } - + if ($this->_currentContext == 'html') { - $body = $this->prettyPrint($body, array("format" => "html")); + $body = self::prettyPrint($body, array('format' => 'html')); } $this->getResponse()->setBody($body); @@ -172,14 +173,8 @@ public function getAutoSerialization() { return $this->_autoSerialization; } - - + /** - * This function is based on the below Zend patches with minor customized changes - * Refs: - * http://framework.zend.com/issues/browse/ZF-9577 - * http://framework.zend.com/issues/browse/ZF-10185 - * * Pretty-print JSON string * * Use 'format' option to select output format - currently html and txt supported, txt is default @@ -189,58 +184,59 @@ public function getAutoSerialization() * @param array $options Encoding options * @return string */ - public function prettyPrint($json, $options = array()) + private static function prettyPrint($json, $options = array()) { $tokens = preg_split('|([\{\}\]\[,])|', $json, -1, PREG_SPLIT_DELIM_CAPTURE); - $result = ""; + $result = ''; $indent = 0; - $format= "txt"; + $format= 'txt'; $ind = "\t"; - if(isset($options['format'])) { + if (isset($options['format'])) { $format = $options['format']; } - switch ($format): + switch ($format) { case 'html': - $line_break = "
"; - $line_break_length = 6; - $ind = "    "; + $lineBreak = '
'; + $ind = '    '; break; default: case 'txt': - $line_break = "\n"; - $line_break_length = 2; + $lineBreak = "\n"; $ind = "\t"; break; - endswitch; + } - //override the defined indent setting with the supplied option - if(isset($options['indent'])) { + // override the defined indent setting with the supplied option + if (isset($options['indent'])) { $ind = $options['indent']; } - - $inLiteral = false; + + $inLiteral = false; foreach($tokens as $token) { - if($token == "") continue; + if($token == '') { + continue; + } $prefix = str_repeat($ind, $indent); if (!$inLiteral && ($token == '{' || $token == '[')) { $indent++; - if($result != "" && substr($result, strlen($result)-$line_break_length) == $line_break) { + if (($result != '') && ($result[(strlen($result)-1)] == $lineBreak)) { $result .= $prefix; } - $result .= "$token$line_break"; + $result .= $token . $lineBreak; } elseif (!$inLiteral && ($token == '}' || $token == ']')) { $indent--; $prefix = str_repeat($ind, $indent); - $result .= "$line_break$prefix$token"; + $result .= $lineBreak . $prefix . $token; } elseif (!$inLiteral && $token == ',') { - $result .= "$token$line_break" ; + $result .= $token . $lineBreak; } else { $result .= ( $inLiteral ? '' : $prefix ) . $token; + // Count # of unescaped double-quotes in token, subtract # of // escaped double-quotes and if the result is odd then we are // inside a string literal diff --git a/library/REST/Controller/Plugin/RestHandler.php b/library/REST/Controller/Plugin/RestHandler.php index 711307933..e762db9c4 100644 --- a/library/REST/Controller/Plugin/RestHandler.php +++ b/library/REST/Controller/Plugin/RestHandler.php @@ -50,104 +50,90 @@ class REST_Controller_Plugin_RestHandler extends Zend_Controller_Plugin_Abstract ); public function __construct(Zend_Controller_Front $frontController) - { - $this->dispatcher = $frontController->getDispatcher(); - } + { + $this->dispatcher = $frontController->getDispatcher(); + } public function dispatchLoopStartup(Zend_Controller_Request_Abstract $request) - { - // only handle Restful WebApi URI - if(strpos($request->getPathInfo(), '/rest/') !== FALSE) - { - // send the HTTP Vary header - $this->_response->setHeader('Vary', 'Accept'); + { + // only handle RESTful API URI + if (strpos($request->getPathInfo(), '/rest/') !== false) { + // send the HTTP Vary header + $this->_response->setHeader('Vary', 'Accept'); - // Cross-Origin Resource Sharing (CORS) - // TODO: probably should be an environment setting? - $this->_response->setHeader('Access-Control-Max-Age', '86400'); - $this->_response->setHeader('Access-Control-Allow-Origin', '*'); - $this->_response->setHeader('Access-Control-Allow-Credentials', 'true'); - $this->_response->setHeader('Access-Control-Allow-Headers', 'Authorization, X-Authorization, Origin, Accept, Content-Type, X-Requested-With, X-HTTP-Method-Override'); + // Cross-Origin Resource Sharing (CORS) + // TODO: probably should be an environment setting? + $this->_response->setHeader('Access-Control-Max-Age', '86400'); + $this->_response->setHeader('Access-Control-Allow-Origin', '*'); + $this->_response->setHeader('Access-Control-Allow-Credentials', 'true'); + $this->_response->setHeader('Access-Control-Allow-Headers', 'Authorization, X-Authorization, Origin, Accept, Content-Type, X-Requested-With, X-HTTP-Method-Override'); - // process module apis - $this->handlePathInfo($request); + // process module APIs + $this->handlePathInfo($request); - $class = $this->getReflectionClass($request); + $class = $this->getReflectionClass($request); - if ($this->isRestClass($class)) - { - // set config settings from application.ini - $this->setConfig(); + if ($this->isRestClass($class)) { + // set config settings from application.ini + $this->setConfig(); - // set response format - $this->setResponseFormat($request); + // set response format + $this->setResponseFormat($request); - // process requested action - $this->handleActions($request); + // process requested action + $this->handleActions($request); - // process request body - $this->handleRequestBody($request); - } + // process request body + $this->handleRequestBody($request); + } } - } + } /** * Parse PathInfo in the orginal request and then alter the original request - * based on the valid Midas Restful URI format: + * based on the valid RESTful API URI format: * /rest[/{moduleName}]/{controllerName}[/{methodName}][/{Id}] * note: [] means optinal parts. */ private function handlePathInfo(Zend_Controller_Request_Abstract $request) - { - $tokens = preg_split('@/@', $request->getPathInfo(), NULL, PREG_SPLIT_NO_EMPTY); - array_shift($tokens); // remove 'rest' prefix - if(!empty($tokens)) - { - if(in_array($tokens[0], Zend_Registry::get('modulesHaveApi'))) - { - $apiModuleName = 'api' . array_shift($tokens); - $controllerName = array_shift($tokens); - $request->setParam('module', $apiModuleName); - $request->setParam('controller', $controllerName); - $request->setModuleName($apiModuleName); - $request->setControllerName($controllerName); - // remove redundant parameter generated by Zend routing - $request->setParam($controllerName, NULL); - } - else - { - array_shift($tokens); // remove controllerName - } - // handle method - if(!empty($tokens) && !is_numeric($tokens[0])) - { - $methodName = array_shift($tokens); - $request->setParam('method', $methodName); - // remove redundant parameter generated by Zend routing - $request->setParam($methodName, NULL); - $request->setParam('id', NULL); - } - // forward to index action if id is not provided - $action = $request->getActionName(); - if(empty($tokens) && ($action == "get" || $action == "index")) - { - $request->setActionName("index"); - } - else if(empty($tokens) && ($action == "post" || $action == "put")) - { - $request->setActionName("post"); - } - else if(!empty($tokens) && is_numeric($tokens[0])) - { - $request->setParam('id', array_shift($tokens)); - } - else - { - $this->_response->setHttpResponseCode(400); //400 Bad Request - throw new Exception('The Webapi ' . $request->getPathInfo() . ' is not supported.', 400); - } + { + $tokens = preg_split('@/@', $request->getPathInfo(), null, PREG_SPLIT_NO_EMPTY); + array_shift($tokens); // remove 'rest' prefix + if (!empty($tokens)) { + if (in_array($tokens[0], Zend_Registry::get('modulesHaveApi'))) { + $apiModuleName = 'api' . array_shift($tokens); + $controllerName = array_shift($tokens); + $request->setParam('module', $apiModuleName); + $request->setParam('controller', $controllerName); + $request->setModuleName($apiModuleName); + $request->setControllerName($controllerName); + // remove redundant parameter generated by Zend routing + $request->setParam($controllerName, null); + } else { + array_shift($tokens); // remove controllerName + } + // handle method + if (!empty($tokens) && !is_numeric($tokens[0])) { + $methodName = array_shift($tokens); + $request->setParam('method', $methodName); + // remove redundant parameter generated by Zend routing + $request->setParam($methodName, null); + $request->setParam('id', null); + } + // forward to index action if id is not provided + $action = $request->getActionName(); + if (empty($tokens) && ($action == 'get' || $action == 'index')) { + $request->setActionName('index'); + } else if (empty($tokens) && ($action == 'post' || $action == 'put')) { + $request->setActionName('post'); + } else if (!empty($tokens) && is_numeric($tokens[0])) { + $request->setParam('id', array_shift($tokens)); + } else { + $this->_response->setHttpResponseCode(400); // 400 Bad Request + throw new Exception('The web API ' . $request->getPathInfo() . ' is not supported.', 400); + } } - } + } private function setConfig() { @@ -176,7 +162,7 @@ private function setResponseFormat(Zend_Controller_Request_Abstract $request) } else { $bestMimeType = $this->negotiateContentType($request); - // if there's no matching MimeType, assign default json + // if there's no matching MimeType, assign default JSON if (!$bestMimeType || $bestMimeType == '*/*') { $bestMimeType = 'application/json'; } @@ -211,7 +197,7 @@ private function handleActions(Zend_Controller_Request_Abstract $request) if ($name == '__CALL' and $method->class != 'Zend_Controller_Action') { $actions[] = $request->getMethod(); - } elseif (substr($name, -6) == 'ACTION' and $name != 'INDEXACTION' and $name != 'CALLCOREACTION') { + } elseif (substr($name, -6) == 'ACTION' and $name != 'INDEXACTION') { $actions[] = str_replace('ACTION', null, $name); } } @@ -361,6 +347,9 @@ private function getReflectionClass(Zend_Controller_Request_Abstract $request) if ($this->reflectionClass === null) { // get the dispatcher to load the controller class $controller = $this->dispatcher->getControllerClass($request); + // if no controller present escape silently... + if ($controller === false) return false; + // ... load controller class $className = $this->dispatcher->loadClass($controller); // extract the actions through reflection diff --git a/library/REST/LICENSE b/library/REST/LICENSE index a789e117b..3e35ea2d9 100644 --- a/library/REST/LICENSE +++ b/library/REST/LICENSE @@ -1,20 +1,21 @@ -Copyright (c) 2011 Code in Chaos Inc. http://www.codeinchaos.com +The MIT License (MIT) -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: +Copyright (c) 2014 Ahmad Nassri. http://ahmadnassri.com -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/library/REST/README.md b/library/REST/README.md new file mode 100644 index 000000000..6d5d6d85e --- /dev/null +++ b/library/REST/README.md @@ -0,0 +1,174 @@ +# RESTful Applications with Zend Framework [![Total views][sourcegraph-image]][sourcegraph-url] + +This extension for Zend Framework, allows to create RESTful Controllers with ease. +please keep in mind that these instructions are general and you should probably customize the code to fit your needs. + +for a working example please refer to [github.com/codeinchaos/restful-zend-framework-example](https://github.com/codeinchaos/restful-zend-framework-example) + +## Assumptions +* you are building a mixed application (regular ZF Controllers + RESTful controllers) +* your controllers can be a a mix of regular controllers and RESTful controllers or a hybrid! + +I recommend creating a separate [module](http://framework.zend.com/manual/1.12/en/zend.controller.modular.html) for the RESTful controllers, its less complicated to manage this way, and you don't have to worry about advanced REST routing ... +However, you can have pie and eat it too! It's possible to have any single Controller act as both a REST controller and a typical Zend MVC controller (HTML output). + +In this particular example, I'm using a separate module "Api" that is purely used for REST API calls. + +## Steps +1. Copy the **REST** directory into your **library**. +2. modify **application.ini**. +3. modify **application/Bootstrap.php**. +4. modify your RESTful module Bootstrap ex: **application/modules/api/Bootstrap.php**. +5. create Controllers as usual, just make sure they extends **REST_Controller**. +6. check https://github.com/codeinchaos/restful-zend-framework-example for examples. +7. reccomended: use the Api_ErrorController provided in the example above, modify to your needs. + +### application.ini: + +add the following: + +```ini +autoloaderNamespaces[] = "REST_" + +rest.default = "xml" +rest.formats[] = "json" +rest.formats[] = "xml" +``` + +the above achieves a couple of things: + +1. Autoloads the REST library +2. sets the default respond format when all content type negotiation fails (in the above example: `xml`) +3. determines the list of content types you want to support in your API, built in types include: `html`, `xml`, `php`, `json` + +### application/Bootstrap.php + +add the following: + +```php +setRequest(new REST_Request); + $frontController->setResponse(new REST_Response); + + // add the REST route for the API module only + $restRoute = new Zend_Rest_Route($frontController, array(), array('api')); + $frontController->getRouter()->addRoute('rest', $restRoute); +} +``` + +In the above example, we are only enabling RESTful responses on a particular route, which is the `Api` module, look up the `Zend_Rest_Route` docs for further configuration options. + +### application/modules/api/Bootstrap.php + +**Note:** depending on your setup, you may want to setup some advanced rules to enable the **REST Plugin** and the **Action Helpers** only when needed. +I use a modified Bootstraping method called "Active Bootstrap" (google it) to only run the bootstrap **_init** methods per active module, which saves me a lot of headaches. + +```php +registerPlugin(new REST_Controller_Plugin_RestHandler($frontController)); + + // add REST contextSwitch helper + $contextSwitch = new REST_Controller_Action_Helper_ContextSwitch(); + Zend_Controller_Action_HelperBroker::addHelper($contextSwitch); + + // add restContexts helper + $restContexts = new REST_Controller_Action_Helper_RestContexts(); + Zend_Controller_Action_HelperBroker::addHelper($restContexts); +} +``` + +## Module Specific ErrorController issue + +It seems there is an inherit issue with Zend Framework's modules & calling the ErrorController, basically ZF calls the default module's error controller for all modules. +This can be a problem of course if one of your modules is an API, you'll end up with HTML in the REST ErrorController output. + +to fix this is beyond the scope of the REST library, so its only included in the README file: + +in your ```application.ini``` + +```ini +resources.frontController.plugins.ErrorHandler.class = "Zend_Controller_Plugin_ErrorHandler" +resources.frontController.plugins.ErrorHandler.options.module = "default" +resources.frontController.plugins.ErrorHandler.options.controller = "error" +resources.frontController.plugins.ErrorHandler.options.action = "error" +``` + +then create a plugin to change the "module" scope, you can name this whatever you want, I went with ```App_Controller_Plugin_Errors```: + +```php +getPlugin('Zend_Controller_Plugin_ErrorHandler'); + + $error->setErrorHandlerModule($request->getModuleName()); + } +} +``` + +## Bugs and feature requests + +Have a bug or a feature request? Please first read the [issue guidelines](CONTRIBUTING.md#using-the-issue-tracker) and search for existing and closed issues. If your problem or idea is not addressed yet, [please open a new issue](https://github.com/codeinchaos/restful-zend-framework/issues/new). + +## Contributing + +Please read through our [contributing guidelines](CONTRIBUTING.md). Included are directions for opening issues, coding standards, and notes on development. + +More over, if your pull request contains JavaScript patches or features, you must include relevant unit tests. + +Editor preferences are available in the [editor config](.editorconfig) for easy use in common text editors. Read more and download plugins at . + +### Contribute and Earn + +Donate bitcoins to this project or make commits and get tips for it. If your commit is accepted by project maintainer and there are bitcoins on its balance, you will get a tip! + +[![tip for next commit][tip4commit-image]][tip4commit-url] + +## Donating + +Donations are welcome to help support the continuous development of this project. + +[![GitTip][gittip-image]][gittip-url] +[![PayPal][paypal-image]][paypal-url] + +## Community + +Keep track of development and updates. + +- Follow [@AhmadNassri](http://twitter.com/ahmadnassri) & [@CodeInChaos](http://twitter.com/codeinchaos) on Twitter. +- Tweet [@CodeInChaos](http://twitter.com/codeinchaos) with any questions/personal support requests. +- Read and subscribe to [My Blog](http://ahmadnassri.com). + +## Authors + +**Ahmad Nassri** + +- Twitter: [@AhmadNassri](http://twitter.com/ahmadnassri) +- Website: [ahmadnassri.com](http://ahmadnassri.com) + +## License + +Licensed under [the MIT license](LICENSE). + +[sourcegraph-url]: https://sourcegraph.com/github.com/codeinchaos/restful-zend-framework +[sourcegraph-image]: https://sourcegraph.com/api/repos/github.com/codeinchaos/restful-zend-framework/counters/views.png +[gittip-url]: https://www.gittip.com/ahmadnassri/ +[gittip-image]: http://img.shields.io/gittip/ahmadnassri.svg +[paypal-url]: https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=UJ2B2BTK9VLRS&on0=project&os0=restful-zend-framework +[paypal-image]: http://img.shields.io/badge/PayPal-Donate-green.svg +[tip4commit-url]: http://tip4commit.com/projects/644 +[tip4commit-image]: http://tip4commit.com/projects/644.svg diff --git a/library/REST/Request.php b/library/REST/Request.php new file mode 100644 index 000000000..28505b42d --- /dev/null +++ b/library/REST/Request.php @@ -0,0 +1,46 @@ +_error = new stdClass; + $this->_error->code = $code; + $this->_error->message = $message; + + return $this; + } + + public function getError() + { + return $this->_error; + } + + public function hasError() + { + return $this->_error !== false; + } + + public function getMethod() + { + if ($this->getParam('_method', false)) { + return strtoupper($this->getParam('_method')); + } + + if ($this->getHeader('X-HTTP-Method-Override')) { + return strtoupper($this->getHeader('X-HTTP-Method-Override')); + } + + return $this->getServer('REQUEST_METHOD'); + } + + public function dispatchError($code, $message) + { + $this->setError($code, $message); + + $this->setControllerName('error'); + $this->setActionName('error'); + $this->setDispatched(true); + } +}