This repository contains a little framework capable of doing a RestAPI easily with PHP
PDOUtils classRepository class in order to use PDOUtilsApiException.php classRest::handleRequest to return a HTTP 409 when exception is raisedRest::existFile functionSingleton Design Pattern
Controller->getAll functionRest::scalePicture functiondbIgnore featureController->getByFields functionsRest::uploadFile function
Rest::$uploadDirRest::getUploadDir()Rest::configureUploadDir()Rest::createDirectoryRecursive()Guards feature
Role EnumerationKeyValue and KeyValueList structure class
Service and Repository classesCredentials classdeleteByField() functiongetByField() function, it returns now Model[] instead of ModelTo start, you'll have to create a folder (I named it /rest)
and place the ApiRest folder inside. The main file will be api.php :
it will create the RestAPI, connect to the DB and listen for requests.
Here is the default content in this file :
<?php
require_once __DIR__ . "/ApiRest/RestAPI.php";
header('Content-Type: application/json');
header('Access-Control-Allow-Headers: Content-Type, enctype, token');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE');
$restAPI = new RestAPI("localhost", "myDB", "root", "");
// You can add an argument to improve the security of your session like this :
//$restAPI = new RestAPI("localhost", "myDB", "root", "", "m*;rO)P7^)3'k[F'S~h0Lx7{zN%`6S");
Rest::configureUploadDir("rest-upload");
echo $restAPI->handleRequest();Now the RestAPI is ready to use. You can check it with Postman and hit the
url with the GET method : http://your-domain/your-path/rest/api.php/user.
It's cool, but how the RestAPI handle that ?
api.php (here it is the word user)user directory existsuser/include.php file existsUserControllerprocessUrl
In the user directory, you will find an example to add paths to your API REST for a User object. Each files related
to a specific object / class / table will have to be in the same folder and follow the same rules as User :
/modelname/
include.phpModelName.controller.phpModelName.model.phpModelName.repository.phpModelName.service.phpControllers are the the first layer called by the RestAPI. They have to be
named like this : modelClassName + "Controller". By default, theses paths
are generated by default :
api.php/modelClassName
api.php/modelClassName/$id
api.php/modelClassName
api.php/modelClassName
api.php/modelClassName/$id
By default, each path generated is associated to the Guard LoginGuard. If you want to remove the guard, you'll have to override the ApiRoute by declaring it in your controller.
You can add a filter on the getAll function by adding GET values on the url. Example :
api.php/modelClassName?lastname=Doe
Here is an example of a UserController class :
<?php
// FILE rest/user/User.controller.php
require_once __DIR__ . "/../ApiRest/Rest.php";
require_once __DIR__ . "/../ApiRest/Controller.php";
class UserController extends Controller {
public function __construct(string $modelName) {
parent::__construct($modelName);
$this->createApiRoute(Rest::PUT, 'login', "login");
$this->createApiRoute(Rest::POST, 'picture', "uploadPicture", [new LoginGuard()]);
}
/* PUT */
public static function login(array $params, stdClass $body): string {
$user = User::fromJSON($body);
return static::$service->login($user);
}
public static function uploadPicture(): string {
$fileName = "htmlInputName";
$newName = "profilePicture.png";
Rest::uploadFile($fileName, $newName);
return "";
}
}When you call createApiRoute() function, the second parameter correspond to
the path of the request. In this string, you can add dynamic parameters like
this 'customPath/$param1/$param2/anything'.
Then, when the associated function will be called, you will find values of
$param1 and $param2 on the $params variable :
var_dump($params);
// print :
// array(
// "param1" => "value1",
// "param2" => "value2",
//)You can override a route with another without removing it, if they don't have the same amount of parameters. For example, theses routes will be kept :
$this->createApiRoute(Rest::GET, '$id', "getById");
$this->createApiRoute(Rest::GET, 'current', "getCurrent");If possible, the route 'current' will be triggered. If it's not, the '$id' route will be triggered .
Services are the next layer called by controllers : They are between Controller
classes and Repository classes. Services are used to get data from Repository
classes and make process the data.
Service classes also can call others services to cross data and make
special process.
By default, functions associated to Controller's paths are generated :
getAll()getByID(string $id)create()update()delete(string $id)Here is an example of a UserService class :
<?php
// FILE rest/user/User.service.php
require_once __DIR__ . "/../ApiRest/Service.php";
class UserService extends Service {
public function login(User $user): string {
return $this->repository->login($user);
}
function initialize() { }
}If you want to use another Service in theses class, you have to declare
it and to initialize the Service class in the initialize function like this:
<?php
class UserService extends Service {
/** @var $bookService BookService */
public $bookService;
public function login(User $user): string {
return $this->repository->login($user);
}
function initialize() {
$this->bookService = BookService::getInstance("Book");
}
}Repositories are the final layer called by Services. They contains SQL queries and get models object from the DataBase.
Same as Services, by default theses functions are available :
getAll()getByID(string $id)create()update()delete(string $id)Here is an example of UserRepository class :
<?php
// FILE rest/user/User.repository.php
require_once __DIR__ . "/../ApiRest/Repository.php";
class UserRepository extends Repository {
public function __construct() {
parent::__construct("user", 'User');
}
public function login(User $user): string {
$fields = new KeyValueList([
new KeyValue("name", $user->name),
new KeyValue("password", $user->password)
]);
$matchedUsers = $this->getByFields($fields);
if (count($matchedUsers) != 1)
throw new Exception("UserRepository->login() : $matchedUsers have a size of " . count($matchedUsers) . " instead of 1 !");
$loggedUser = $matchedUsers[0];
if ($loggedUser)
return Rest::IDToToken($loggedUser->id);
else
return "";
}
}If you want to use custom SQL queries, you can use PDOUtils class in order to execute them. Here is an example :
$query = "< CUSTOM_SELECT_QUERY >";
$PDOStatement = PDOUtils::executeQuery($query);
return static::getByPDOStatement($PDOStatement);There some functions you can use from PDOUtils :
executeQuery(query)
executeQueryWithParameter(query, keyValue)
executeQueryWithParameters(query, KeyValueList)
Models are the classes representing a line in the DataBase.
When data are retrieved from the DataBase, booleans won't be equals to
true/false but to '1'/'0'. To get back our stolen booleans, you can
call the function preserveBooleans() to convert boolean fields.
To mark a field like a boolean field, just call this function inside the
constructor : addBoolean("booleanField1", "booleanField2", ...)
On the same way, you can mark a field as jsonIgnore by calling the function
addIgnore("ignoredField1", ...). Then, to convert Model's instances to
JSON call the encode() function !
There is another function : addDbIgnore("field1", ...) used to specify which
fields are not stored in the dataBase.
Model classes can be retrieved from JSON using this static function :
Model::fromJSON(stdClass $data).
Guards are used to restrict data access depending on the user's role. Three Guards are created by default :
AdminGuardModeratorGuardLoginGuardThey all extends the Guard abstract class, and implements the authorizeAccess(): bool function. You can create your
own guard too following the same rules, and then adding a new line in the include.php file in the Guard directory.
Guards are called in the Controllers classes, when you will declare the route. Note that a route can have 0, 1 or multiple guards.
Exception can be raised when there is an issue to be printed displayed in the front-end website. To do so, you have to
throw an ApiException and the server will respond with a HTTP 409 error.
The ApiException class extends the Exception class, but it does not use parameters from the superclass.
This class is made in a way to easily use some internationalization Frondend tool: it's containing a key parameter
which can be the key of the translation in the JSON file, and a parameters parameter which contain the data to be
transmitted. For example, here is an ApiException and the associated JSON internationalization file :
throw new ApiException("api-error.error1", ["name" => 'John']);{
"api-error": {
"error1": {
"title": "Hey !",
"text": "Hello {{name}}"
}
}
}When using an ApiException, these data will be stored in the error property of the HttpErrorResponse JavaScript
item.
Add the capacity of forcing the type of each Model's field Otherwise, No future improvements planned now. Doesn't mean that the package will not be updated !