respect / rest Goto Github PK
View Code? Open in Web Editor NEWThin controller for RESTful applications
Home Page: http://respect.github.io/Rest
License: Other
Thin controller for RESTful applications
Home Page: http://respect.github.io/Rest
License: Other
Busy fixing the tests after reintroducing the factory params needing an example that can take string, array as arguments I thought awesome ArrayObject will do the trick but lo and behold, for some weird and wonderful reason 'ArrayObject" 'offsetSet' is not callable, even though it works on the instance and method_exists sees it to be so. Strange?
var_export(method_exists('ArrayObject', 'offsetSet'));
// output: true
var_export(is_callable(array(new ArrayObject, 'offsetSet')));
// output: true
var_export(is_callable(array('ArrayObject', 'offsetSet')));
// output: false
Performance is also an issue as bob at thethirdshift dot net will tell you on PHP.net
Benchmark results:
Did 10000 is_callables in 0.0640790462494 seconds
Did 10000 function_exists in 0.0304429531097 seconds
We would have to do a combination of is_array, function_exist, method_exist; and then produce the benchmarks to see how it would affect us but if it fails in this example where else would it be mistaken?
I believe the Router should have a default route for not found routes.
As a feature seen and described in codeguy/Slim#205, a possible usage:
<?php
$r3->get('/text/*', function($filename) {
return fopen('data/'.$filename, 'r+');
})->acceptEncoding(array(
'deflate' => function($stream) {
stream_filter_append($stream, 'zlib.deflate', STREAM_FILTER_READ);
return $stream; //now deflated on demand
}
));
Provide them in the public
folder.
Currently binding controller classes like below:
$r3->any('/', 'HomeController');
causes fatal errors. Its not indicated in the README that these classes should implement interface Routable.
Had to go deeply through the code to figure that out. Please update the README or set an option for users to choose
whether they want it this way.
PHP 5.3.9 ZendServer 5.6
installed via pear
<?php
echo get_include_path();
use Respect\Rest\Router;
$r3 = new Router;
$r3->get('/', function() {
return 'Hello World';
}
);
include_path is
.:/usr/local/zend/share/ZendFramework/library:/usr/local/zend/share/pear
Error reported is
.:/usr/local/zend/share/ZendFramework/library:/usr/local/zend/share/pear
I did verify that Respect/... is installed in /usr/local/zend/share/pear
Thanks for your help.
When you register a route, you can have optional parameters, like /users/*
, if you declare in the function that the parameter is optional, than if none is given the route matches and is dispatched. This doesn't happen with classes used as routes. #sadpanda
$route->createUri
returns an URL for any route. Currently, the virtual host set on Router isn't part of that URL, we need to include it.
That's it.
Fix exception message in line 26 of Routines/AbstractAccept.php
As an example, the given code provided by @ramsey.
<?php
/**
* In PHP 5.4.0, this works perfectly. In PHP 5.3.3, it creates a fatal error:
*
* PHP Fatal error: Function name must be a string
*/
class Foo
{
public function getBar($bar)
{
return $bar;
}
}
$foo = new Foo();
$methodCall = array($foo, 'getBar');
if (is_callable($methodCall)) {
echo $methodCall('bar');
}
After looking into 2295 and 2296 it has become evident that we cannot treat these headers separately when doing coneg.
For example the link lists returned, the "q" value calculations and in a broader sense any part of analyzing and handling these headers are done as a whole and not in isolation. You don't respond to the content type of the Accept header alone, you also consider the language, encoding, features etc.
Will we not better be served if we combine these headers instead of treating them like any other header?
Example concern, but everything else is the same really:
As a first step in the local variant selection algorithm, the overall qualities associated with all variant descriptions in the list are computed.
The overall quality Q of a variant description is the value
Q = round5( qs * qt * qc * ql * qf * qa )
where rounds5 is a function which rounds a floating point value to 5 decimal places after the point. It is assumed that the user agent can run on multiple platforms: the rounding function makes the algorithm independent of the exact characteristics of the underlying floating point hardware.
The factors qs, qt, qc, ql, qf, and qa are determined as follows.
As an example, if a variant list contains the variant description
{"paper.2" 0.7 {type text/html} {language fr}}
and if the configuration database contains the quality value assignments
types: text/html;q=1.0, type application/postscript;q=0.8
languages: en;q=1.0, fr;q=0.5
then the local variant selection algorithm will compute the overall quality for the variant description as follows:
{"paper.2" 0.7 {type text/html} {language fr}}
| | |
| | |
V V V
round5 ( 0.7 * 1.0 * 0.5 ) = 0.35000
With same configuration database, the variant list
{"paper.1" 0.9 {type text/html} {language en}},
{"paper.2" 0.7 {type text/html} {language fr}},
{"paper.3" 1.0 {type application/postscript} {language en}}
would yield the following computations:
round5 ( qs * qt * qc * ql * qf * qa ) = Q
--- --- --- --- --- ---
paper.1: 0.9 * 1.0 * 1.0 * 1.0 * 1.0 * 1.0 = 0.90000
paper.1: 0.7 * 1.0 * 1.0 * 0.5 * 1.0 * 1.0 = 0.35000
paper.3: 1.0 * 0.8 * 1.0 * 1.0 * 1.0 * 1.0 = 0.80000
When running this piece of code:
<?php
require 'bootstrap.php'; // PSR-0 autoloader in there
class RouteKnowsGet implements \Respect\Rest\Routable {
public function get($param) {
return "ok: $param";
}
}
class RouteKnowsNothing implements \Respect\Rest\Routable {
}
$_SERVER['HTTP_ACCEPT'] = 'application/json';
$router = new \Respect\Rest\Router();
$router->isAutoDispatched = false;
$r1 = $router->any('/meow/*', 'RouteKnowsGet');
$r1->accept(array('application/json' => 'json_encode')); // some routine inheriting from AbstractAccept
$router->any('/moo/*', 'RouteKnowsNothing');
$out = $router->run(new \Respect\Rest\Request('get', '/meow/blub')); // ReflectionException
echo "I survived! Result is: '$out'\n";
I get the following exception:
PHP Fatal error: Uncaught exception 'ReflectionException' with message 'Method RouteKnowsNothing::GET() does not exist' in /home/fusselwurm/dev/src/Rest/library/Respect/Rest/Routes/ClassName.php:43
Stack trace:
#0 /home/fusselwurm/dev/src/Rest/library/Respect/Rest/Routes/ClassName.php(43): ReflectionMethod->__construct('RouteKnowsNothi...', 'GET')
#1 /home/fusselwurm/dev/src/Rest/library/Respect/Rest/Request.php(75): Respect\Rest\Routes\ClassName->getReflection('GET')
#2 /home/fusselwurm/dev/src/Rest/library/Respect/Rest/Routes/AbstractRoute.php(102): Respect\Rest\Request->routineCall('when', 'GET', Object(Respect\Rest\Routines\Accept), Array)
#3 /home/fusselwurm/dev/src/Rest/library/Respect/Rest/Router.php(197): Respect\Rest\Routes\AbstractRoute->matchRoutines(Object(Respect\Rest\Request), Array)
#4 /home/fusselwurm/dev/src/Rest/library/Respect/Rest/Router.php(215): Respect\Rest\Router->dispatchRequest(Object(Respect\Rest\Request))
#5 /home/fusselwurm/dev/src/Rest/tests/moo2.php(24): Respect\Rest\Router->run(Object(Respect\R in /home/fusselwurm/dev/src/Rest/library/Respect/Rest/Routes/ClassName.php on line 43
A routine that receives an IP address and must return false
if not being rate-limited or an integer with number of seconds to retry. Implement 429 status code as well.
Consider returning 405 Method Not Allowed
when the route exists but the requested HTTP method is not implemented for the route. An Allow
header must also be included that lists the implemented/allowed methods. See http://tools.ietf.org/html/rfc2616#section-10.4.6.
This was identified on a improvised mini-workshop session with @marcelonovaes and @knorrium.
Routines in Respect\Rest are abstracted in routine classes. The magic builder that allows you to create them and assign them to Route instances like $route->foo($param)
leading to $route->appendRoutine(new Routines\Foo($param))
currently accepts just one parameter, so $route->bar($param, $other)
actually leads to an inconsistent $route->appendRoutine(new Routines\Bar($param)
, without the second argument.
This bug was introduced back when we created the first routine with two parameters. I believe it was the AuthBasic one. Fix should be simple.
Convert PHP errors and uncaught exceptions into 500 status codes. 408 may be used for timeout if we could figure out a way to calculate max_execution_time.
Routes inside routes respect their parents patterns by concatenating them. Parents acts as hubs:
<?php
$router = new Respect\Rest\Router;
$router->any('/users/*', function($username=null) {
return showMyUserPlease($username);
})->children(
$router->any('/lists', function($username=null) {
return showMyUserLists($username);
})
);
Sample above would match /users/alganet
and /users/alganet/lists
and route each url to its proper children.
Children could be reused:
<?php
$router = new Respect\Rest\Router;
$listsRoute = $router->any('/lists', function($username=null) {
return showMyUserLists($username);
}
$router->any('/users/*', function($username=null) {
return showMyUserPlease($username);
})->children($listsRoute);
$router->any('/admins/*', function($username=null) {
return showMyUserPlease($username);
})->children($listsRoute);
A new empty route type could be created to be just a namespace for other routes:
<?php
$router = new Respect\Rest\Router;
$router->any('/users/*')->children(
$router->any('/lists', function($username=null) {
return showMyUserLists($username);
}),
$router->any('/followers', function($username=null) {
return showFollowers($username);
})
);
Sample above will match /users/alganet/lists
and /users/alganet/followers
, but will not match just /users/alganet
.
Hey mate, how is it going?
I think I may have found a bug.
Check it out.
The method AbstractRoute::match(), does not use the Accept to check whether or not the route is correct.
Example
<?php
$router = new Router();
$router->get('/images', 'MyApp\Controller\ImageJson')->accept(array('application/json' => $jsonHandler));
$router->get('/images', 'MyApp\Controller\ImageHtml')->accept(array('text/html' => $htmlHandler));
In this example, if you have an Accept: text/html,application/json
, it'll always use the first route.
Classes doesn't support the any method as they do for: get, post, put and delete. This is a very handy catchall function and should be available on classes as well as on functions.
Assigning a ProxyableWhen event to a route that maps to an unexistent class will fail.
This was identified on a improvised mini-workshop session with @marcelonovaes and @knorrium.
We were developing on Mac OS X Lion using the bundled PHP 5.3.10 and bundled Apache with no php.ini or httpd.conf changes. Setting $router->autoDispatched = false
had no effect at all, dispatching the router anyway. I have no idea why yet.
A routine that can map routes to specific rendering engines (such as template engines or serializers).
This is a topic I've been burning to raise but there are still features required that takes a higher priority than this. Since the RESTful API architecture involves extensive CACHE considerations there is not much need to further motivate the benefit of having a properly implemented CACHE solution at the end-point, aside from the obvious performance and load shedding advantages, to be able to verify and test conformance of the developed REST implementation without the need for external CACHE providersp
It is only until now that we are discussing conditional requests that this has become pertinent as the framework will see a huge benefit if it is able to cache and make request provisioning decisions based on cached variants rather than re-requesting the Routers to populate a response on every request. for state information.
2616 allows for ETag to be used as an "opaque" identifier instead of dates:
The ETag response-header field value, an entity tag, provides for an "opaque" cache validator. This might allow more reliable validation in situations where it is inconvenient to store modification dates, where the one-second resolution of HTTP date values is not sufficient, or where the origin server wishes to avoid certain paradoxes that might arise from the use of modification dates.
Although I this will not be sufficient as the only identifier and we will benefit from having an additionally indexed key when ETag is not available, based on a calculated checksum of the following HTTP artifacts:
(Of the top of my head this will have to be verified)
location
accept header
method
In the interim it is safe to speculate that we would consider plug-able cache provisioning utilizing some/all of the following :
via file cache - simple and free from dependencies utilizing PHPs file_put/file_get_contents();
via PDO as database driven cache
via APC provisioned
via REDIS provisioned
via Memcache provisioned
via APC provisioned
via Solr provisioned
via ElasticSearch provisioned
via Reverse Proxy (Squid probably) provisioned (not sure if this is exactly what we have in mind though.
Aside from the obvious requirement that the design should be flexible to easily cater for additions to this list of providers I would additionally see the functionality being transparently exposing an API that can be utilized by other components of the application where this functionality may be required without impacting the core cache.
What are your thoughts?
I have a route like above:
$r3->get('/foo/bar/*/*', function($table, $record) ...
And when I hit the service in /foo/bar/sales/product.name.value
.
The $record parameter is coming with 'product' value and not the whole value. -.-
I created a route that looks like this:
/accounts/*/catalogs/*
According to the documentation, that first asterisk is not an optional parameter. However, requests to /accounts/catalogs
is matching on that. Shouldn't it be giving me a 404 or something?
Reproduction script:
<?php
include '../vendor/autoload.php';
$r3 = new \Respect\Rest\Router();
$r3->get('/accounts/*/catalogs/*', function($accountId, $catalogId = null) {
echo "Account ID: {$accountId}, Catalog ID: {$catalogId}";
});
Request:
GET /accounts/catalogs HTTP/1.1
Host: localhost:8181
Content-Type: application/x-www-form-urlencoded; charset=utf-8
Accept-Encoding: identity, deflate, compress, gzip
Accept: */*
User-Agent: HTTPie/0.1.7-dev
Response:
HTTP/1.1 200 OK
Date: Thu, 10 May 2012 17:39:41 GMT
Server: Apache/2.2.20 (Ubuntu)
X-Powered-By: PHP/5.4.0-3~oneiric+4
Vary: Accept-Encoding
Content-Encoding: gzip
Content-Length: 42
Content-Type: text/html
Account ID: , Catalog ID:
What is the difference in using the by approach versus just putting the code in the beginning of the route? What would be better is being able to register a by routine and have it apply to all routes. These two approaches shown below work the same way.
$r1 = new Router;
$r1->get('/alert/key/*', function($keyValue) {
$status = new AlertStatus();
return $status->getStatus(Config::$alertMsgFileXml);
})->by(function($keyValue) {
$users = new Users(Config::$alertUserListXml,$keyValue);
if (!$users->isKeyValid()){
header('HTTP/1.1 403 Forbidden');
return "get out of here";
}
});
// the above works the same way as below
$r1->get('/alert/key/*', function($keyValue) {
$users = new Users(Config::$alertUserListXml,$keyValue);
if (!$users->isKeyValid()){
header('HTTP/1.1 403 Forbidden');
return "get out of here";
}
$status = new AlertStatus();
return $status->getStatus(Config::$alertMsgFileXml);
});
Thanks
Eric Palmer
Forward to another route by instance:
<?php
$router = new Respect\Rest\Router;
$foo = $router->any('/foo', function() {
return array('alganet', 'php', 'respect');
});
$bar = $router->any('/bar', function() use($foo) {
if ($baz)
return 'Hi';
else
return $foo;
});
When a route is returned inside another route, the framework should use it as a forward (internal, without http redirects). On any(), the method should be the same originated in the request.
Some clients don't support PUT or DELETE very well. For these cases, consider reading the optional "_method" query string parameter and supporting "PUT" and "DELETE" values only if the current HTTP method is POST. The routing should work exactly as if the client had used the HTTP PUT or DELETE methods.
See page 451 of RESTful Web Services by Richardson and Ruby for reference on "Overloading POST."
We currently don't have real unit tests, our tests are more functional/BBDish. I'm gonna write the proper units and keep the old tests as legacy, already started!
Consider the following code example using the authBasic()
routine:
<?php
$r3 = new \Respect\Rest\Router();
$r3->get('/', function() {
echo 'Hello';
})->authBasic('My Realm', function($user, $pass) {
return $user === 'admin' && $pass === 'pass';
});
When I query this without providing basic auth headers, I get the correct 401 Authorization Required
response that I expect:
HTTP/1.1 401 Authorization Required
Date: Tue, 15 May 2012 22:55:38 GMT
Server: Apache/2.2.20 (Ubuntu)
X-Powered-By: PHP/5.4.0-3~oneiric+4
WWW-Authenticate: Basic realm="My Realm"
Vary: Accept-Encoding
Content-Encoding: gzip
Content-Length: 20
Content-Type: text/html
However, if I do provide authentication headers, but the username and password are incorrect, causing the authBasic()
routine to return false
, then I get back a 200 OK
response with no content.
HTTP/1.1 200 OK
Date: Tue, 15 May 2012 22:57:08 GMT
Server: Apache/2.2.20 (Ubuntu)
X-Powered-By: PHP/5.4.0-3~oneiric+4
Vary: Accept-Encoding
Content-Encoding: gzip
Content-Length: 20
Content-Type: text/html
There is no security concern here, since you are properly blocking the client from receiving the content, but the 200 OK
doesn't seem appropriate. Perhaps a 403 Forbidden
is more appropriate by default, or we can continue to leave it up to the application developer.
Here's an example of how I'm accomplishing this right now:
<?php
$r3 = new \Respect\Rest\Router();
$r3->get('/', function() {
echo 'Hello';
})->authBasic('My Realm', function($user, $pass) {
if ($user === 'admin' && $pass === 'pass') {
return true;
} else {
header('HTTP/1.1 403 Forbidden');
return false;
}
});
I believe __toString() on Router should dispatch and run it.
We could create a CliRequest that when injected into the Router instead of the traditional Request could route things via Cli calls. Bash and linux are somewhat RESTful. An experimental mocked cli request is in the Router test suite for a long time now, but it isn't very friendly for users.
Considering a $r->get('/users/*', 'MyUsersController')
sample route, both a HTTP GET /users/alganet
and a php application.php get users alganet
cli route should route to the same point. Custom route verbs could be used, such as $r->install('/', 'MyInstallController');
.
Much of the code from the current Request class would be moved to an AbstractRequest that would be extended to both Request and CliRequest.
As written in the php documentation "Attempting to throw an exception from a destructor (called in the time of script termination) causes a fatal error."
Router.php destructor throws an exception when theres an error. This causes a very unhelpful fatal error message:
"Fatal error: Exception thrown without a stack frame in Unknown on line 0"
Fixed it by wrapping it in a try-catch block
if (!$this->autoDispatched || !isset($_SERVER['SERVER_PROTOCOL']))
return;
try {
$response = $this->dispatch();
if ($response)
echo $response->response();
} catch (\Exception $e) {
echo $e->getTraceAsString(); //or something more meaningful instead
}
@alganet on #php-respect 20120711
dunno yet I'd like to see $router->any('/messages/*')->linkTo($mailboxRoute);
something between the route objects
but I'm not sure about the API
When() routines are great for parameter validation. They're executed after the path match, so we could throw a 400 Bad Request if a when() fails and no other route matches.
Allow, for example, to know which className the current ClassRoute has.
I try to use "When Routine" as the link below. Always returns a 400 status.
But when I remove the call GET "docs", it works.
Maybe the user may need to provide a closure for that, we can pass him the choices.
Assuming I have the following route defined:
<?php
$r3->get('/', 'MyClass')->accept(array(
'application/json' => 'json_encode',
));
When it sends the content back to the client, the Content-Type
header is still using PHP's default type of text/html
(or whatever is defined as default in your php.ini). If I manually set the header()
in MyClass::get()
, then I can return application/json
as the Content-Type
, but if I have multiple types I want to accept (i.e. application/xml
), then this won't work.
Please consider automatically setting the Content-Type
header to the appropriately matched Accept
type.
Our tests are messy, declaring header
functions everywhere, as of PHP 5.4 we can use http_response_code function to check headers and xdebug_get_headers together to check for HTTP header changes on our tests.
Implementing our own header
abstraction would pose work without much benefit - other than having better tests.
composer.json
(we could go higher).assertHttpStatusCode
assertion.assertHeaderContains
assertion.Issues #18 and #25 showed we need to manipulate the HTTP header sometimes, despite the correction of the issues we should implement some way to handle header manipulation in a way that fits inside the Respect/Rest/Router and feels right.
"How we are going to do that?" is the question that this issue should fix, hopefully with a nice and sweet push =D
With all those 206 and 416 nice guys.
As seen on fabpot/Silex#183 a routine for asserting User-Agent strings would be quite useful and simple to implement.
<?php
$r3->get('/about', function() {
return array('v' => 2.0);
})->acceptLanguage(array(
'en' => function($data) { return array("Version" => $data['v']); },
'pt' => function($data) { return array("Versão" => $data['v']); }
))->userAgent(array(
'Songbird (\d\.\d)[\d\/]*?'=>function($data) { return $data; },
'(msie) ([\w.]+)' => function($data) { return $data; },
'(mozilla)(?:.*? rv:([\w.]+))?' => function($data) { return $data; }
'*' => function($data) { return false; } // Don't respond for any other userAgent
));
As noted in RequestTest see @76230e77928d9a6ebbae6977aaf02edf4cdf45a2
//TODO change ->uri to ->path, populate other parse_url keys
//TODO same behavior for env vars and constructor params regarding parse_url
Automatic generation by MD5ing the content. Custom logic can be implemented separately.
The documentation suggests in Controller splitting that you may log the request path like such:
<?php
$logRoutine = function() use ($myLogger, $r3) {
$myLogger->logVisit($r3->request->path);
};
$r3->any('/users', 'UsersController')->by($logRoutine);
$r3->any('/products', 'ProductsController')->by($logRoutine);
But Router does not expose the request neither does request expose any path, request does have the uri exposed as public though.
This is an intuitive approach to this common use case, I suggest we adjust the source code to reflect the documentation.
This feature is dependent on the implementation of the ETag see #40
Implement capabilities required for conditional requests:
Because these requests require knowledge of the actual response and to prevent us having to re-compose the response for every question this will benefit considerably from a server side cache solution as suggested in #60
Did I miss anything?
What are your thoughts, suggestions, special requirements? Do you know of any current implementations we might benefit from referencing? Warnings? Concerns? Are you able to help?
Get involved and you can also be part of making Respect/Rest even more awesome than it is, there is more than enough ways for you to help, feel free to contact any of the Respect developers if you need help getting involved!
Also a big thank you to our current supporters you guys rock! Keep up the good work!
If I have something like that
public function testAccept()
{
$this->object->always('Accept', array('application/json' => function($data) {
return 'ha';
}));
$request = new Request('get', '/users/alganet');
$_SERVER['HTTP_ACCEPT'] = 'application/json';
$this->object->get('/users/*', function() {
return range(0, 10);
})->accept(array('application/json' => function ($d)
{
var_dump($d);
}));
$r = $this->object->dispatchRequest($request)->response();
$this->assertEquals(json_encode(range(0, 10)), $r);
}
The always accept is execute before the accept of the route.
In my opinion, olny the route accept shloud be run, not both.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.