Open scottux opened 11 years ago
looks great! Have you already tested it in a production environment?
I wouldn't use it as-is in production, it has worked well in a secure intraweb, but would need authentication somehow if exposed to the world, unless the data you are serving is freely available. It also doesn't do JSONP for remote client calls, which is fine if you are using a proxy between the client and the server. Like I said, it is pretty simplistic but curl commands work great, local and proxied Ajax calls work and you can run any method defined in your model.
Also, I dug up an old comment by @meecect http://qcu.be/content/qcubed-and-json that may help with this feature.
This simple ajax call should work against the codegen'ed examples if your file is named rest.php.
(function ($) {
var restPath = 'rest.php';
var userId = 1;
$.ajax({
url: restPath+"/Person/LoadById/"+userId,
method: "GET",
success: function (data) { alert("Username: "+data.Username); },
error: function (xhr) { alert(xhr.responseText.error); }
});
}(jQuery));
As an update, this is what I am currently thinking. It changes the interface to be /model/id/submodel/id
rather than just exposing all of the methods of the ORM. The CRUD actions are determined by the HTTP verb anyway. This builds an api where you should be able to load /Person/1/Task
for a list of Person 1's tasks.
I haven't started exposing HATEOAS links yet, but that is on my todo list.
<?php
/* Include the most awesome PHP library ever. */
require("assets/qcubed.inc.php");
/* The first part of the URL defines the base model to load. */
$model = ucfirst(QApplication::PathInfo(0));
/* The second part of the URL defines the guid of the model. */
$modelId = QApplication::PathInfo(1);
/* The third part of the URL defines a sub-model. */
$subModel = ucfirst(QApplication::PathInfo(2));
/* The third part of the URL defines a sub-model guid. */
$subModelId = QApplication::PathInfo(3);
/* Attempt a GET request, handle the error */
function tryGet($model, $modelId='', $subModel='', $subModelId=''){
$output = null;
if (!$modelId && !$subModel && !$subModelId){
$output = $model::LoadAll();
} elseif ($modelId && !$subModel && !$subModelId){
$output = $model::Load($modelId);
} elseif ($modelId && $subModel && !$subModelId){
$obj = $model::Load($modelId);
$method = 'Get'.$subModel.'Array';
if(method_exists($obj,$method)){
$output = $obj->$method();
}else{
$property = $subModel.'Object';
$output = $obj->$property;
}
} elseif ($modelId && $subModel && $subModelId){
$output = $subModel::Load($subModelId);
}
if ($output){
if (is_array($output)){
$myOutput = array();
foreach ($output as $stuff){
$myOutput[] = $stuff->getIterator();
}
}else{
$myOutput = (is_object($output)) ? $output->getIterator() : $output;
}
header('Content-type: application/json');
return json_encode($myOutput);
}else{
throw new Exception("The requested resource could not be located.", 1);
}
}
/* For error responses. */
function throwError($code, $message){
header($_SERVER['SERVER_PROTOCOL'].' '.$code);
echo '{"error":"'.$message.'"}';
}
/* Determine method to use */
switch($_SERVER['REQUEST_METHOD']){
// Read
case 'GET':
if(!$model && !$modelId && !$subModel && !$subModelId){
// Show documentation
echo "<h1>REST API</h1><p>This service will accept:</p> <ul><li>GET - /model/id/submodel/id</li><li>PUT - /model/id</li><li>POST - /model</li><li>DELETE - /model/id</li></ul>";
}else{
try{
echo tryGet($model, $modelId, $subModel, $subModelId);
}catch(Exception $e){
throwError('404 Not Found', $e->getMessage());
}
}
break;
// Update
case 'PUT':
if (!$modelId){
throwError('406 Not Acceptable',"ID is required");
}else{
$output = $model::LoadById($modelId);
// Loop through data and set the fields.
parse_str(file_get_contents("php://input"), $post_vars);
foreach($post_vars as $post => $value){
$output->$post = $value;
}
$output->Save();
echo $output->getJson();
}
break;
// Create
case 'POST':
if ($modelId){
throwError('406 Not Acceptable',"Perhaps you meant to PUT?");
}else{
$output = new $model();
// Loop through data and set the fields.
foreach($_POST as $post=>$value){
$output->$post = $value;
}
$output->Save();
header($_SERVER['SERVER_PROTOCOL'].' 201 Created');
echo $output->getJson();
}
break;
// Yep, delete
case 'DELETE':
if (!$modelId){
throwError('406 Not Acceptable',"ID is required");
}else{
$output = $model::LoadById($modelId);
$output->Delete();
echo '{"message":"deleted '.$modelId.'"}';
}
break;
// Currently unused
case 'HEAD':
case 'OPTIONS':
break;
// What you talkin' 'bout Willis?
default:
header('HTTP/1.0 501 Not Implemented');
die();
}
If the only goal is to provide CRUD, then your suggestion might be ok
(eventhough /person/1/task/5 is not really useful, since I could have just
done /task/5).
However, I think we should also provide some (perhaps limitted) search
functionality like this:
/model/prop1/value1/prop2/value2/... This way instead of /person/1/task,
you write /task/person/1 (i.e. get all the tasks where person=1). We can
get as sophisticated as we want with both properties and values. Properties
should support nesting: /task/person.firstName/jane
. Values should
support different syntaxes for handling as many query operators as
possible. For exsmple /task/person.firstName/~%an%/locaton/CA,NY,TX would
do a LIKE query on firstName and an IN query on location.
Having the search functionality will allow us to keep CRUD simple, just /model/id.
What do you think?
On Monday, December 9, 2013, Scott wrote:
As an update, this is what I am currently thinking. It changes the interface to be /model/id/submodel/id rather than just exposing all of the methods of the ORM. The CRUD actions are determined by the HTTP verb anyway. This builds an api where you should be able to load /Person/1/Taskfor a list of Person 1's tasks.
I haven't started exposing HATEOAS links yet, but that is on my todo list.
<?php/* Include the most awesome PHP library ever. /require("assets/qcubed.inc.php");/ The first part of the URL defines the base model to load. /$model = ucfirst(QApplication::PathInfo(0)); / The second part of the URL defines the guid of the model. /$modelId = QApplication::PathInfo(1); / The third part of the URL defines a sub-model. /$subModel = ucfirst(QApplication::PathInfo(2)); / The third part of the URL defines a sub-model guid. /$subModelId = QApplication::PathInfo(3); / Attempt a GET request, handle the error */function tryGet($model, $modelId='', $subModel='', $subModelId=''){ $output = null;
if (!$modelId && !$subModel && !$subModelId){ $output = $model::LoadAll(); } elseif ($modelId && !$subModel && !$subModelId){ $output = $model::Load($modelId); } elseif ($modelId && $subModel && !$subModelId){ $obj = $model::Load($modelId); $method = 'Get'.$subModel.'Array'; if(method_exists($obj,$method)){ $output = $obj->$method(); }else{ $property = $subModel.'Object'; $output = $obj->$property; } } elseif ($modelId && $subModel && $subModelId){ $output = $subModel::Load($subModelId); }
// if($modelId && $modelId){// $output = $model::$method($id);// }elseif($method && !$id){// $output = $model::$method();// }else{// $output = $model::LoadAll();// }
if ($output){ if (is_array($output)){ $myOutput = array(); foreach ($output as $stuff){ $myOutput[] = $stuff->getIterator(); } }else{ $myOutput = (is_object($output)) ? $output->getIterator() : $output; } header('Content-type: application/json'); return json_encode($myOutput); }else{ throw new Exception("The requested resource could not be located.", 1); }}/* For error responses. */function throwError($code, $message){ header($_SERVER['SERVER_PROTOCOL'].' '.$code); echo '{"error":"'.$message.'"}';}/* Determine method to use */switch($_SERVER['REQUEST_METHOD']){ // Read case 'GET': if(!$model && !$modelId && !$subModel && !$subModelId){ // Show documentation echo "<h1>REST API</h1><p>This service will accept:</p> <ul><li>GET - /model/id/submodel/id</li><li>PUT - /model/id</li><li>POST - /model</li><li>DELETE - /model/id</li></ul>"; }else{ try{ echo tryGet($model, $modelId, $subModel, $subModelId); }catch(Exception $e){ throwError('404 Not Found', $e->getMessage()); } } break; // Update case 'PUT': if (!$modelId){ throwError('406 Not Acceptable',"ID is required"); }else{ $output = $model::LoadById($modelId); // Loop through data and set the fields. parse_str(file_get_contents("php://input"), $post_vars); foreach($post_vars as $post => $value){ $output->$post = $value; } $output->Save(); echo $output->getJson(); } break; // Create case 'POST': if ($modelId){ throwError('406 Not Acceptable',"Perhaps you meant to PUT?"); }else{ $output = new $model(); // Loop through data and set the fields. foreach($_POST as $post=>$value){ $output->$post = $value; } $output->Save(); header($_SERVER['SERVER_PROTOCOL'].' 201 Created'); echo $output->getJson(); } break; // Yep, delete case 'DELETE': if (!$modelId){ throwError('406 Not Acceptable',"ID is required"); }else{ $output = $model::LoadById($modelId); $output->Delete(); echo '{"message":"deleted '.$modelId.'"}'; } break; // Currently unused case 'HEAD': case 'OPTIONS': break; // What you talkin' 'bout Willis? default: header('HTTP/1.0 501 Not Implemented'); die();}
— Reply to this email directly or view it on GitHubhttps://github.com/qcubed/framework/issues/253#issuecomment-30182025 .
Ok, so to get all the persons on task 1, /person/task/1
rather than /task/1/person
. That could definitely work. Let me make some adjustments and see what I can come up with.
I like the idea of the search, that's probably going to take another iteration or three. Can you post some more examples of how you would like to see the endpoints?
I have the following rough code for a REST API:
I am curious to know if this is helpful or if anyone has done anything along these lines. I have used the SOAP service but found it kludgy, this is pretty minimalistic but will produce a decent API.