qcubed / qcubed

The official QCubed framework git repo
https://qcubed.eu/
57 stars 47 forks source link

QCubed and REST #253

Open scottux opened 11 years ago

scottux commented 11 years ago

I have the following rough code for a REST API:

<?php
/* Include the most awesome PHP library ever. */
require("qcubed.inc.php");
/* The first part of the URL defines the model to use. */
$model = ucfirst(QApplication::PathInfo(0));
/* The second part of the URL defines the method of the model to use. */
$method = QApplication::PathInfo(1);
/* The third part of the URL defines the parameter of the method. */
$id = QApplication::PathInfo(2);
/* Attempt a GET request, handle the error  */
function tryGet($model,$method='',$id=''){
    $output = null;

    if($method && $id){
        $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 && !$id && !$method){
            // Show documentation
            echo "<h1>REST API</h1><p>This service will accept:</p> <ul><li>GET - /Model/Method/Parameter</li><li>PUT - /Model/Id</li><li>POST - /Model</li><li>DELETE - /Model/Id</li></ul>";
        }else{
            try{
                echo tryGet($model,$method,$id);
            }catch(Exception $e){
                throwError('404 Not Found', $e->getMessage());
            }
        }
        break;
    // Update
    case 'PUT':
        if (!$id){
            throwError('406 Not Acceptable',"ID is required");
        }else{
            $output = $model::LoadById($id);
            // 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 ($id){
            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 (!$id){
            throwError('406 Not Acceptable',"ID is required");
        }else{
            $output = $model::LoadById($id);
            $output->Delete();
            echo '{"message":"deleted '.$id.'"}';
        }
        break;
    // Currently unused
    case 'HEAD':
    case 'OPTIONS':
        break;
    // What you talkin' 'bout Willis?
    default:
        header('HTTP/1.0 501 Not Implemented');
        die();
}
?>

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.

olegabr commented 11 years ago

looks great! Have you already tested it in a production environment?

scottux commented 11 years ago

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.

scottux commented 11 years ago

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));
scottux commented 10 years ago

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();
}
vakopian commented 10 years ago

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 .

scottux commented 10 years ago

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.

scottux commented 10 years ago

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?