PHP Framework for Learning

Checkpoint

2017-11-25 release

2021-03-12 update


Object-Orientation

abstract class
parent class
abstract
parent class
abstract method
child class child class
(abstract method)
not instantiation only declaration necessary instantiation definition
Application getRootDir
registerRoutes
MiniBlogApplication getRootDir
registerRoutes
Controller AccountController
StatusController
DbRepository UserRepository
StatusRepository
FollowingRepository
Framework Mini Blog Application
extends
$this in parent class (instance of child class)
e.g. Application.php line 23 etc
$this->setDebugMode($debug); $this = MiniBlogApplication
e.g. Controller.php line 26 etc
$this->controller_name = strtolower(substr(get_class($this), 0, -10)); $this = AccountController
$this = StatusController
e.g. DbRepository.php line 19 etc
$this->setConnection($con); $this = UserRepository
$this = StatusRepository
$this = FollowingRepository

ClassLoader

See Process flow | User registration

method process
register spl_autoload_register(array($this, 'loadClass'));
  register loadClass in autoload stack
registerDir $this->dirs[0]: C:\XAMPP\xampp_5.6.31\htdocs\mini-blog.localhost/core
$this->dirs[1]: C:\XAMPP\xampp_5.6.31\htdocs\mini-blog.localhost/models
loadClass read class in core and models (except ClassLoader)
autoload when unloaded class in core and models is new or extends,
execute loadClass in autoload stack

Front controller and .htaccess

<IfModule mod_rewrite.c>
    RewriteEngine On
    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteRule ^(.*)$ index.php [QSA,L]
</IfModule>

C:\XAMPP\xampp_5.6.31\htdocs\mini-blog.localhost\web\.htaccess

process: if specified file does not exist, access all index.php

Request

BaseUrl, PATH_INFO

http://mini-blog.localhost/
REQUEST_URI SCRIPT_NAME BaseUrl PATH_INFO
/ /index.php /
/status/post /index.php /status/post
/user/user1 /index.php /user/user1
/user/user1/status/1 /index.php /user/user1/status/1
/account /index.php /account
/account/signup /index.php /account/signup
/follow /index.php /follow

REQUEST_URI - BaseUrl = PATH_INFO
IDE (NetBeans IDE 8.2) unused

http://mini-blog.localhost/index.php/
REQUEST_URI SCRIPT_NAME BaseUrl PATH_INFO
/index.php/ /index.php /index.php /
/index.php/status/post /index.php /index.php /status/post
/index.php/user/user1 /index.php /index.php /user/user1
/index.php/user/user1/status/1 /index.php /index.php /user/user1/status/1
/index.php/account /index.php /index.php /account
/index.php/account/signup /index.php /index.php /account/signup
/index.php/follow /index.php /index.php /follow

REQUEST_URI - BaseUrl = PATH_INFO
IDE (NetBeans IDE 8.2) unused

$_POST (getPost)

CSRF
key value (example)
['_token'] 'ed38d8176bff02c2b0364055da5e8cb4d72fc85d'

<input type="hidden" name="_token" value="<?php echo $this->escape($_token); ?>" />

Router

method process
__construct __construct($definitions)    $definitions = $this->registerRoutes()
reference Application.php line 54 (new Router($this->registerRoutes()))
reference MiniBlogApplication.php line 17 (protected function registerRoutes())
compileRoutes create $this->routes↓ from Routing definition
resolve search $this->routes↓ and specify controller and action from PATH_INFO
Routing definition (MiniBlogApplication)
PATH_INFO controller action
/ status index
/status/post status post
/user/:user_name status user
/user/:user_name/status/:id status show
/account account index
/account/:action account
/follow account follow
$this->routes ($this = Router)
pattern $params['controller'] $params['action']
/ status index
/status/post status post
/user/(?P<user_name>[^/]+) status user
/user/(?P<user_name>[^/]+)/status/(?P<id>[^/]+) status show
/account account index
/account/(?P<action>[^/]+) account Note
/follow account follow

Note identify action
    (Router.php line 62) preg_match('#^' . $pattern . '$#', $path_info, $matches)
    e.g. set  $matches['action']=>'signup'↓ from /account/signup
named subpattern: (?P<user_name>[^/]+) etc

$matches
$path_info $matches ($params)

/user/user1
/user/user2
/user/user3
(except controller, action)
$matches['user_name']=>'user1'
$matches['user_name']=>'user2'
$matches['user_name']=>'user3'

/user/user1/status/1
/user/user1/status/2
/user/user2/status/3
/user/user2/status/4
/user/user3/status/5
(except controller, action)
$matches['user_name']=>'user1', $matches['id']=>'1'
$matches['user_name']=>'user1', $matches['id']=>'2'
$matches['user_name']=>'user2', $matches['id']=>'3'
$matches['user_name']=>'user2', $matches['id']=>'4'
$matches['user_name']=>'user3', $matches['id']=>'5'
/account/signup
/account/register
/account/signin
/account/authenticate
/account/signout
$matches['action']=>'signup'
$matches['action']=>'register'
$matches['action']=>'signin'
$matches['action']=>'authenticate'
$matches['action']=>'signout'

See Preparation | Test Data

dynamic parameter
regular expression (named subpattern) matched string
/user/(?P<user_name>[^/]+) /user/user1
/user/user2
/user/user3
/user/(?P<user_name>[^/]+)/status/(?P<id>[^/]+) /user/user1/status/1
/user/user1/status/2
/user/user2/status/3
/user/user2/status/4
/user/user3/status/5
/account/(?P<action>[^/]+) /account/signup
/account/register
/account/signin
/account/authenticate
/account/signout

See Preparation | Test Data

Response

property Application.php (runAction)
$content response->setContent($content)

DbManager

connect (connection information)
$name PDO instance Note Note
master $con = new PDO dsn mysql:dbname=mini_blog;host=localhost;charset=utf8
user root
password
options

set connection information (MiniBlogApplication.php line 39, DbManager.php line 20)
Note information to pass to constructor of PDO class

$this->connections
key ($name) value
master $con (PDO instance)
$this->repository_connection_map unused (multiple databases)
key ($repository_name) value ($name)
Status master
User master
Following master

setRepositoryConnectionMap($repository_name, $name)

$this->repositories
key ($repository_name) value ($repository) method (DbManager)
User UserRepository instance db_manager->get('User')
Status StatusRepository instance db_manager->get('Status')
Following FollowingRepository instance db_manager->get('Following')

DbRepository

table class
user table UserRepository
status table StatusRepository
following table FollowingRepository
__construct($con)
property setConnection($con) DbManager
$con UserRepository->con
StatusRepository->con
FollowingRepository->con
DbManager->connections['master'] = $con
(PDO instance)
method method implementation
execute($sql, $params = array()) $stmt = $this->con->prepare($sql);
$stmt->execute($params);
return $stmt;
($stmt: PDOstatement instance)
fetch($sql, $params = array()) return $this->execute($sql, $params)->fetch(PDO::FETCH_ASSOC);
fetchAll($sql, $params = array()) return $this->execute($sql, $params)->fetchAll(PDO::FETCH_ASSOC);

prepare (escape values in placeholders)

PDO::FETCH_ASSOC (receive result as associative arrays)

Session

static property process
static $sessionStarted
  self::$sessionStarted
check that session_start() is not called more than once
static $sessionIdRegenerated
  self::$sessionIdRegenerated
check that session_regenerate_id(true) is not called more than once

it can be used without being instantiated (variable's value is retained even if process moves outside function)

$_SESSION

user (Login user)
key value
['user']['id'] '1'
['user']['user_name'] 'user1'
['user']['password'] 'ee5281d035bd1bd7786301be4274a68b006ae916'
['user']['created_at'] 2017-11-01 00:00:00

session->get('user')
session->set('user', $user)
Login User ID: user1, Password: password
See Preparation | Test Data

_authenticated (Authentication, Login status)
key value
['_authenticated'] true(Login) or false(Not Login)

session->setAuthenticated()
session->isAuthenticated()

CSRF
key value (example)
['csrf_tokens/account/signin'][0] ... 'ed38d8176bff02c2b0364055da5e8cb4d72fc85d'
['csrf_tokens/account/signin'][9] 'bfb55f8dd338b02efea1ad1a12c249b722b499ae'
['csrf_tokens/account/signup'][0] ... '117d0e6d9da8ec9e595110d7e517993901ee77ce'
['csrf_tokens/account/signup'][9] '17fbad25e479cdc7e712b9977d2b9d9fdb85ecb3'
['csrf_tokens/account/follow'][0] ... '4b710f10e11206dd0e5ba2b23fea9c993b20a6c3'
['csrf_tokens/account/follow'][9] '77e5617a7d149f6f4774f4f607affaa3a171cdc9'
['csrf_tokens/status/post'][0] ... '62138aa5b21213242eee2135892b9589609cc9d6'
['csrf_tokens/status/post'][9] '27da715c32fe321f52f20ddf0a45e462a611edf3'
CSRF (e.g. 'account/signin')
method process
session->get('csrf_tokens/account/signin', array()) $_SESSION['csrf_tokens/account/signin'] --> $tokens
session->set('csrf_tokens/account/signin', $tokens) $tokens --> $_SESSION['csrf_tokens/account/signin']

Application

load PHP script
PHP script require_once/require
/controllers require_once Application.php (line 231)  findController
require_once doesn't read a file that has been read once
(it prevents function redefinition and variable reassignment)

Mini Blog Application
  AccountController.php
  StatusController.php
/core require ClassLoader.php (line 40)  loadClass
Framework
/models require ClassLoader.php (line 40)  loadClass
get use DbManager->repositories[]  (DbManager.php  get)
  it creates an instance only first time
    same effect as require_once
    (it prevents function redefinition and variable reassignment)

Mini Blog Application
  UserRepository.php
  StatusRepository.php
  FollowingRepository.php
property instance
MiniBlogApplication->request Request
MiniBlogApplication->response Response
MiniBlogApplication->session Session
MiniBlogApplication->db_manager DbManager
MiniBlogApplication->router Router

Controller

Controller.php (line 26)
$this->controller_name = strtolower(substr(get_class($this), 0, -10))
function return value
strtolower(substr(get_class($this), 0, -10)) 'user' ($this->controller_name)
get_class($this) (e.g. $this: UserController) 'UserController'
substr('UserController', 0, -10) 'User' (Controller: 10 characters)
strtolower('User') 'user'
$this->controller_name
$this return value
'AccountController' 'account'
'StatusController' 'status'
property property (Application) instance
AccountController->request MiniBlogApplication->request Request
AccountController->response MiniBlogApplication->response Response
AccountController->session MiniBlogApplication->session Session
AccountController->db_manager MiniBlogApplication->db_manager DbManager
StatusController->request MiniBlogApplication->request Request
StatusController->response MiniBlogApplication->response Response
StatusController->session MiniBlogApplication->session Session
StatusController->db_manager MiniBlogApplication->db_manager DbManager
CSRF (e.g. 'account/signin')
method process
generateCsrfToken('account/signin') $tokens Max 10
generate $token (SHA-1 hash value), set $token in $tokens
return $token

'_token' => $this->generateCsrfToken('account/signin')
checkCsrfToken('account/signin', $token) true: if there is $token in $tokens, delete $token
false: None
$auth_actions (actions requiring login)
value
AccountController array('index', 'signout', 'follow')
StatusController array('index', 'post')
- true (All actions required login) unused

View

HTML structure

Account Registration (User Registration)

layout.php (common with all screens)

render execution3, output3 (View::render in View::render), $_layout = false

$base_url

$session

$title

$_content

signup.php

render execution1, output2 (View::render in Controller::render), $_layout = 'layout'

$base_url

$user_name

$password

$_token

inputs.php

render execution2, output1 (View::render in signup.php), $_layout = false

$user_name

$password

Error screen

errors.php (common with all error screens)

inputs - signup - layout  comparison

inputs - signup  comparison

(click to enlarge)

signup - layout  comparison

(click to enlarge)

output buffering (function to buffer output information internally)

function process
ob_start() start output buffering
ob_implicit_flush(0) disable automatic flashing of buffer (finally buffer is output)
ob_get_clean get contents of current buffer and delete output buffer

render

__construct
View->base_dir C:\XAMPP\xampp_5.6.31\htdocs\mini-blog.localhost/views
View->defaults['request'] Request
View->defaults['base_url'] ''
View->defaults['session'] Session
ccount Registration (User Registration)
View->render method (inputs.php) execution2, output1
array_merge extract require inputs.php
View->defaults['request'] $request
View->defaults['base_url'] $base_url
View->defaults['session'] $session
$_variables['user_name'] $user_name $user_name ''
$_variables['password'] $password $password ''
$_variables['_token'] $_token

$content = ob_get_clean() ... inputs.php

View->render method (signup.php) execution1, output2
array_merge extract require signup.php
View->defaults['request'] $request
View->defaults['base_url'] $base_url $base_url ''
View->defaults['session'] $session
$_variables['user_name'] $user_name $user_name ''
$_variables['password'] $password $password ''
$_variables['_token'] $_token $_token e.g.
'e1968c9ccc99acfccadd1ff7a524f1eb74c18ec1'

$content = ob_get_clean() ... signup.php + inputs.php

View->render method (layout.php) execution3, output3
array_merge extract require layout.php
View->defaults['request'] $request
View->defaults['base_url'] $base_url $base_url ''
View->defaults['session'] $session $session Session
$_variables['title'] $title $title 'Account Registration'
$_variables['_content'] $_content $_content signup.php + inputs.php

echo $_content
$content = ob_get_clean() ... layout.php + signup.php + inputs.php

Homepage
View->render method (status.php) execution2, output1
array_merge extract require status.php
View->defaults['request'] $request
View->defaults['base_url'] $base_url $base_url ''
View->defaults['session'] $session
$_variables['status']['id'] $status['id'] $status['id'] '1'
$_variables['status']['user_id'] $status['user_id']
$_variables['status']['body'] $status['body'] $status['body'] 'status1 user1 test1'
$_variables['status']['created_at'] $status['created_at'] $status['created_at'] '2017-11-01 00:00:00'
$_variables['status']['user_name'] $status['user_name'] $status['user_name'] 'user1'

foreach (Contribution status1-status4)
$content = ob_get_clean() ... status.php

View->render method (index.php) execution1, output2
array_merge extract require index.php
View->defaults['request'] $request
View->defaults['base_url'] $base_url $base_url ''
View->defaults['session'] $session
$_variables['statuses'] $statuses $statuses Contribution status1-status4 4 cases
$_variables['body'] $body $body ''
$_variables['_token'] $_token $_token e.g.
'e1968c9ccc99acfccadd1ff7a524f1eb74c18ec1'

foreach (Contribution status1-status4)
$content = ob_get_clean() ... index.php + status.php

View->render method (layout.php) execution3, output3
array_merge extract require layout.php
View->defaults['request'] $request
View->defaults['base_url'] $base_url $base_url ''
View->defaults['session'] $session $session Session
$_variables['title'] $title $title 'Home'
$_variables['_content'] $_content $_content index.php + status.php

foreach (Contribution status1-status4)
echo $_content
$content = ob_get_clean() ... layout.php + index.php + status.php

Error screen
controller action controller->render ($template)
(input screen)
AccountController registerAction signup
authenticateAction signin
StatusController postAction index

Exception

HttpNotFoundException (Error)

HttpNotFoundException.php
<?php
 
class HttpNotFoundException extends Exception {};
 

Application.php run (throw)  No route found

e.g. No route found for /account

177            if ($params === false) {
178                throw new HttpNotFoundException('No route found for ' . $this->request->getPathInfo());
179            }

Application.php runAction (throw)  controller is not found

e.g. AccountController controller is not found.

209        if ($controller === false) {
210            throw new HttpNotFoundException($controller_class . ' controller is not found.');
211        }

Controller.php forward404 (throw)  Error

e.g. Forwarded 404 page from account/register

94    protected function forward404()
95    {
96        throw new HttpNotFoundException('Forwarded 404 page from '
97            . $this->controller_name . '/' . $this->action_name);
98    }


Error (forward404)
class method check error
Controller run if (!method_exists($this, $action_method)) {
    $this->forward404();
}
No class method
AccountController registerAction
authenticateAction
followAction
if (!$this->request->isPost()) {
    $this->forward404();
}
Not POST
followAction if (!$following_name) {
    $this->forward404();
}
No following name
if (!$follow_user) {
    $this->forward404();
}
No follow user
StatusController postAction if (!$this->request->isPost()) {
    $this->forward404();
}
Not POST
userAction if (!$user) {
    $this->forward404();
}
No user
showAction if (!$status) {
    $this->forward404();
}
No status
Application.php run (catch)  Error display on browser
185        } catch (HttpNotFoundException $e) {
186            $this->render404Page($e);

Note NetBeans Debugging methods (Change Variable value to change Process flow) See

UnauthorizedActionException (Transition to Login)

UnauthorizedActionException.php
<?php
 
class UnauthorizedActionException extends Exception {};
 

Controller.php run (throw)  Login Necessary && Not Login (Deny false → true)
53        if ($this->needsAuthentication($action) && !$this->session->isAuthenticated()) {
54            throw new UnauthorizedActionException();
55        }

Application.php run (catch)  Login screen

runAction('account', 'signin')

187        } catch (UnauthorizedActionException $e) {
188            list($controller, $action) = $this->login_action;
189            $this->runAction($controller, $action);
190        }

Note Process flow (Account information management and Login) See

Mini Blog Application

method (in AccountController, StatusController)
function class method
Check Controller checkCsrfToken($form_name, $token) Token check
Request isPost() POST check
Session isAuthenticated() Login status check
FollowingRepository isFollowing($user_id, $following_id) true (1, 2)
false (1, 3)
   Note !isFollowing
null (1, 1)
UserRepository isUniqueUserName($user_name) user name
   duplication check
Data set Controller generateCsrfToken($form_name) generate Token
Session clear() Logout
Session set($name, $value) Login user
   ('user', $user)
Session setAuthenticated($bool) Login status true, false
Data get DbManager get($repository_name) 'Following'
   FollowingRepository
'Status'
   StatusRepository
'User'
   UserRepository
Request getPost($name) 'user_name'
'password'
'_token'
'body'
'following_name'
Session get($name) Login user ('user')
StatusRepository fetchAllByUserId($user_id) status1-status2
status3-status4
status5
StatusRepository fetchAllPersonalArchivesByUserId($user_id) 'user1' (2 cases),
   'user2' (2 cases)
StatusRepository fetchByIdAndUserName($id, $user_name) status1
UserRepository fetchAllFollowingsByUserId($user_id) Following 'user2'
UserRepository fetchByUserName($user_name) 'user1' 1record
'user2' 1record
'user3' 1record
'user4' 1record
UserRepository hashPassword($password) 'password'
Data output FollowingRepository insert($user_id, $following_id) 1, 3
StatusRepository insert($user_id, $body) 1, 'status6 user1 test6'
UserRepository insert($user_name, $password) 'user4', 'password'
Screen Controller forward404()
Controller redirect($url)
Controller render($variables = array(), $template = null, $layout = 'layout')

See Preparation | Test Data

Page Top
Page Bottom