A custom PHP 7 application created side-by-side with Lambert Mata in order to help interns become familiar with the MVC design pattern.
The current Vagrantfile
have the ncaro/php7-debian8-apache-nginx-mysql box.
The Web Server must be configured to route every request to index.php
file present in the root code
folder. If you are using the Vagrantfile
here the nginx
sample:
location / {
# Route all the URIs to index.php
try_files /index.php?$args $uri /index.php?$args;
# Avoid file caching for Development.
sendfile off;
add_header Last-Modified $date_gmt;
add_header Cache-Control 'no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0';
if_modified_since off;
expires off;
etag off;
proxy_no_cache 1;
proxy_cache_bypass 1;
...
}
In order to use the MySQL
database, you must change the root password.
SET PASSWORD = PASSWORD('xxxxxxxx');
Since the application will search fot a database called app
, you must create one.
CREATE DATABASE app;
The application will start once the file app.php
is included.
The application will start reading the file routes.php
, containing all the available routes with their own HTTP Verbs
.
Each route
consist of three things:
-
Method
- The method that the route should respond, it can be:
POST,GET,PATCH or DELETE
.
- The method that the route should respond, it can be:
-
URI
- The Uniform Resource Identifier that the route should respond, it can be anything separated from
/
.
- The Uniform Resource Identifier that the route should respond, it can be anything separated from
-
Action
- The Action is what the route must do when called, it can be tree things:
Closure
PHP Anonymous FunctionString
Simple StringArray
Must have the associationuse
pointing to an existingController
andmethod
separated by@
.
- The Action is what the route must do when called, it can be tree things:
The Closure
or Method
will receive as first argument an instance of Request
.
Router::get('/',['use'=>'Controller@method']);
Router::post('/post/something',function(Request $request){
/*Code goes here*/
});
When the Request
object is ready, then the application will execute a handle
method that will search for the requested Route
, if there is any. If the Route is found, then the defined Action
will be executed, passing as first parameter the Request
object.
Those are the handling implementations in case the Action
has been declared as:
-
String
- It should get directly printed.
-
Closure
- The Closure should be executed passing as first argument the
Request
object.
- The Closure should be executed passing as first argument the
-
Array
- The specified
Method
in the specifiedController
should be called, passing as first argument theRequest
object.
- The specified
If one of the routes have been wrongly written, the application should not start.
A Route
can have an infinite amount of white cards in order to facilitate the URI implementation. The white card is created using a mustached syntax {id}
.
Router::delete('/user/{id}',['use'=>'UserController@delete']);
This declared Route
will match a DELETE
request http://localhost/user/10
.
Each white card will be added as argument to the defined Closure
or Controller
.
Router::delete('/user/{id}',function(Request $request,$id){
Users::delete('id',$id);
});
Once the routes
have been parsed and validated, the application will capture the incoming request and save all the information in the Request
instance.
The Request
instance should contain all the information regarding the incoming request,correctly separated and categorized in Parameter
instances.
The Request
instance will have the following accessible properties.
public $method; // Request method | String
public $attributes; //The request attributes parsed from the PATH_INFO | Parameter
public $request; //Request body parameters ($_POST). | Parameter
public $query; //Query string parameters ($_GET). | Parameter
public $server; //Server and execution environment parameters ($_SERVER). | Parameter
public $files; //Uploaded files ($_FILES). | Parameter
public $cookies; //Cookies ($_COOKIE). | Parameter
public $headers; //Headers (taken from the $_SERVER). | Parameter
public $content; //The raw Body data | String
If the Controller returns a json or text , you can use the Response
instance.
It will set the correct Content-type
and return the desired data format, currently the implemented have the following Content-types
:
application/json
withjson
static method.text/plain
withtext
static method.
/**
* Return the desired HTTP code with json
*/
public static function json($content=[],int $status=self::HTTP_OK,$flags=JSON_FORCE_OBJECT|JSON_NUMERIC_CHECK)
/**
* Return the desired HTTP code with text
*/
public static function text(string $content='',int $status=self::HTTP_OK)
/**
* Return the desired HTTP code
*/
public static function code(int $status=self::HTTP_OK)
Each Parameter
instance will have the following accessible methods:all
,keys
,replace
,add
,get
,set
,has
,remove
.
public function all(): array //Returns the parameters.
public function keys(): array //Returns the parameter keys.
public function replace(array $parameters = array()) //Replaces the current parameters by a new set.
public function add(array $parameters = array()) //Add parameters.
public function get(string $key, $default = null) //Returns a parameter by name, or fallback to default.
public function set(string $key, $value) //Sets a parameter by name.
public function has(string $key): bool //Returns true if the parameter is defined.
public function remove(string $key) //Removes a parameter.
/* http://localhost/info?beans=10 */
Router::get('/info',function(Request $request){
return $request->query->has('beans') ?
json_encode($request->query->get('beans')) : [];
});
A custom Controller
must be created in order to properly work ,it should look like this.
class CustomController extends Controller{
public function index(Request $request){
/*Code goes here*/
return json_encode($request->content);
}
}
If a
Controller
is specified in theroutes.php
file and not found, the application should not start.
If the Controller
returns a Web page, then the View
class should be required
and returned as response with the desired data.
class CustomController extends Controller{
public function index(Request $request){
$keys = $request->query->keys();
return new View('home.php',compact('keys'));
}
}
The abstract Model
class allows the mapping of Relational Database to Objects without the need to parse data manually.
Subclasses of Model can perform basic CRUD
operations and access the entity relationship as simple as calling a method.
Those operations are provided using the
Database
class from theQuery Builder Project
.
The Model
must support One to One
, One to Many
and Many to Many
relationships.
/**
* Create a one to one relationship
* @param The Entity name is the class name to map the results to
* @returns The Model The one to one entity for the current instance
* */
public function hasOne(EntityName)
/**
* Creates a one to many relationship
* @param The Entity name is the class name to map the results to
* @returns An array of the one to many entities for the current instance
* */
public function hasMany(EntityName)
/**
* Commits to database the current changes to the entity
* @returns bool Success status
* */
public function save():bool
/*E.g.*/
$todo = Todo::first('id', 1);
$todo->title = "New title";
$todo->save();
/**
* Deletes the current instance from the database
* @returns bool Success status
* */
public function delete():bool
/*E.g.*/
$todo = Todo::first('id', 1);
$todo->delete();
/** Configures the database connection */
public static function configConnection($host, $dbName, $user, $password)
/*E.g.*/
Model::configConnection(
'host',
'db_name',
'user',
'pass'
);
/** Deletes from the database using the input condition */
public static function destroy($col1, $exp, $col2): bool
/*E.g.*/
Todo::destroy('id', '=', 1);
/**
* Query the first result of a table using column value pair
* @returns Model of first query result
*/
public static function first($col1, $col2)
/*E.g.*/
Todo::first('id', 1);
/**
* Query the entity result of a table using expression
* @returns An array of Model from the query result
*/
public static function find($col1, $col2)
/*E.g.*/
Todo::find('id', 1);
/**
* Query all the table values
* @returns An array of mapped entities
*/
public static function all()
/*E.g.*/
Todo::all();
/**
* Applies a where condition to the table
* @returns Statement Query Statement using the Model table
*/
public static function where($col1, $exp, $col2)
/*E.g.*/
Todo::where('title', 'My title')->get()
/**
* Creates a new value in the database using data array (column + value)
* @returns Model The new instance on success
*/
public static function create(array $data)
/*E.g.*/
Todo::create(['title' => 'My title']);
To enable relationship mapping, create a method to return the entities and use the following methods allow to map the
entity relationship to the current instance. The methods take the className
as parameter.
One to One
class Article { //The author of an article
public function author() {
return $this->hasOne('User');
}
}
One to Many
and Many to Many
class User { //The author articles
public function articles() {
return $this->hasMany('Articles');
}
}
Before making any queries it is necessary to setup database configuration using static function configConnection
.
Model::configConnection('host', 'dbName', 'user', 'password');
Model class cannot be instantiated as it is, but must be extended by another class
- The table name is inferred from the class
ClassName
to tableclass_name
by converting fromPascalCase
tosnake_case
.- Foreign keys must follow the
<table>_id
name convention.- Entity tables must have an
id
field.- In order to work, mapped tables must have an
id
column
Extend Model
to map an existing table to the class.
class Author extends Model {
/*Code goes here*/
}
Class Author
is mapped to a table named author
.
Now static and instance methods can be called on Author
.
// Create a new record in author table
$author = Author::create([
// id is not specified as it has auto increment
'name' => 'Satoshi',
'username' => 'john123'
]);
// Update
$author->name = 'Nakamoto';
$author->save();
// Delete
$author->delete();
// or
Author::delete('name', '=', 'John');
CREATE TABLE tweet (
id INT(8) UNSIGNED AUTO_INCREMENT PRIMARY KEY,
content VARCHAR(280) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
CREATE TABLE comment (
id INT(8) UNSIGNED AUTO_INCREMENT PRIMARY KEY,
tweet_id INT(6) UNSIGNED,
content VARCHAR(280) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (tweet_id) REFERENCES tweet(id) ON DELETE NO ACTION ON UPDATE NO ACTION
);