This repository contains a little framework capable of doing a RestAPI easily with PHP
- 21 Dec. 2019
- Added
PDOUtils
class - Cleaned
Repository
class in order to usePDOUtils
- Added
- 19 Dec. 2019
- Updated isLogged function to check the token validity
- 15 Dec. 2019
- Improved route detection to use the one with less parameters
- 15 Jul. 2018
- Added exception feature
- Added
ApiException.php
class - Updated
Rest::handleRequest
to return a HTTP 409 when exception is raised
- Added
- Added exception feature
- 11 Jul. 2018
- Added
Rest::existFile
function
- Added
- 23 Jun. 2018
- Improved Services with the
Singleton
Design Pattern- This is fixing circular includes on services constructor
- Improved Services with the
- 6 Jun. 2018
- Updated the way that route are checked, and now allow static route to override route with parameters
- 24 May 2018
- Added GET parameters support for
Controller->getAll
function
- Added GET parameters support for
- 5 March 2018
- Added
Rest::scalePicture
function - Added
dbIgnore
feature
- Added
- 22 Sept. 2017
- Added
Controller->getByFields
functions - Added
Rest::uploadFile
function- Added
Rest::$uploadDir
- Added
Rest::getUploadDir()
- Added
Rest::configureUploadDir()
- Added
Rest::createDirectoryRecursive()
- Added
- Added
- 22 Oct. 2017
- Added
Guards
feature- Added
Role
Enumeration
- Added
- Added
KeyValue
andKeyValueList
structure class- Updated
Service
andRepository
classes
- Updated
- Added
Credentials
class
- Added
- 23 Oct. 2017
- Added
deleteByField()
function - Updated
getByField()
function, it returns nowModel[]
instead ofModel
- Added
- RestAPI Framework
- Model Superclass
- contains models, why asking ?
- Controller Superclass
- Here is where you will bind url / function
- Service Superclass
- Here is where you'll call the Repository
- Repository Superclass
- Get the data from the DB
- Guards feature
- Used to protect your data with access rights
- KeyValueList & KeyValue Classes
- Used to filter your requests
- Model Superclass
- JWT Token
- File transfer
To 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 ?
- Get the word after
api.php
(here it is the worduser
) - Check if
user
directory exists - Check if
user/include.php
file exists - Create the controller associated, named
UserController
- Call the Controller's method
processUrl
- Verify the url match
- Check if guards are okay
- Call the associated function
- Process query(ies) and return the result
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.php
ModelName.controller.php
ModelName.model.php
ModelName.repository.php
ModelName.service.php
Controllers 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 :
- GET
api.php/modelClassName
- Get all data
- Filterable with GET parameters
- GET
api.php/modelClassName/$id
- Get by ID
- POST
api.php/modelClassName
- Create data
- PUT
api.php/modelClassName
- Update data
- DELETE
api.php/modelClassName/$id
- Delete data with 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)
- Execute a SQL query
executeQueryWithParameter(query, keyValue)
- Execute a SQL query with one parameter
executeQueryWithParameters(query, KeyValueList)
- Execute a SQL query with several parameters
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 :
AdminGuard
ModeratorGuard
LoginGuard
They 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 !