PHP without Frameworks

'98: First project on the web

  • Lyrics website
  • PHP 3
  • Microsoft IIS, Microsoft SQL Server, Microsoft Windows
<?
mssql_connect("localhost", "lyrics", "...") or DIE("Cannot connect to db");
mssql_select_db("lyrics");

$result = mssql_query("SELECT id, title FROM lyrics WHERE artist = " . $artist_id);
?>
<TABLE><TR><TH>Title</TH></TR>
<?
while ($row = mssql_fetch_array($result, MSSQL_ASSOC)) {
    printf(
        "<TR><TD><A href="view_song.php?id=%s">%s</A></TD></TR>",
        $row["id"], $row["name"]
    );
}
mssql_free_result($result);
?>
</TABLE>
http://www.phpthewrongway.com/
namespace AppBundle\Controller;

use Parody\Component\HttpFoundation\Response;

class ArtistController {
  public function listSongsAction($artistId) {
    mssql_connect("localhost", "lyrics", "...") or DIE("Cannot connect to db");
    mssql_select_db("lyrics");

    $result = mssql_query("SELECT id, title FROM lyrics WHERE artist = " . $artistId);
    $html = "<TABLE><TR><TH>Title</TH></TR>";
    while ($row = mssql_fetch_array($result, MSSQL_ASSOC)) {
      $html .= "<TR><TD><A href="view_song.php?id=$row[id]">$row[name]</A></TD></TR>";
    }
    $html .= "";
    mssql_free_result($result);

    return new Response($html);
  }
}

To framework

or not to framework?

What is a framework?

#TODO: Put a decent definition here.
count($frameworkDefinitions) ≅ count($frameworks);

Common framework parts:

  • Libraries/components: avoid reinventing the wheel
  • Architecture for your application
  • Coding standards, guidelines, best practices...
  • Dependency Injection (Container)
  • Routing
  • Templating

Framework advertising

"A framework is not absolutely necessary: it is “just” one of the tools that is available to help you develop better and faster!"
"[...] a framework provides you with the certainty that you are developing an application that is in full compliance with the business rules, that is structured, and that is both maintainable and upgradable."

Framework advertising

"Guaranteed upgradability and maintenance"
"This is the basic principle of a framework: Not having to reinvent the wheel."

PHP is unique!

From the beginning: designed primarily for web development.

C/C++, Java, Python, Ruby, ... are all good and proven languages, that generally requires a web framework for web development.

A sample "Twitter-like" PHP 7.1 application

Coding standards

  • Define your standards, it may include existing ones like PSR-*
  • Use PHP_CodeSniffer to detect violations and create your Sniffs

Routing

A long time ago in a galaxy far far away...:

  • / (index.php)
  • /users.php?id=...
  • /tweets.php?id=...

Typical Apache setup

RewriteEngine On

# Serve assets directly
RewriteRule ^/css/.*\.css - [L]
RewriteRule ^/js/.*\.js - [L]

# Redirect all requests to a single entry point
RewriteRule . /index.php

Funnel everything to a single Front Controller

Front Controller will route to the right Controller/Action

"No Funnel" Apache setup:

RewriteEngine On

# http://sample/
RewriteRule ^/$ /index.php [L]

# http://sample/<user>
RewriteRule ^/([^/]+)$ /user.php?id=$1 [L]

# http://sample/<user>/status/<tweet>
RewriteRule ^/([^/]+)/status/([^/]+)$ /tweet.php?user=$1&id=$2 [L]

Routing on the HTTP method


# GET http://sample/<user>/status/<tweet>
RewriteCond %{REQUEST_METHOD} =GET
RewriteRule ^/([^/]+)/status/([^/]+)$ /tweet_get.php?user=$1&id=$2 [L]

# POST http://sample/<user>/status/<tweet>
RewriteCond %{REQUEST_METHOD} =POST
RewriteRule ^/([^/]+)/status/([^/]+)$ /tweet_post.php?user=$1&id=$2 [L]

# DELETE http://sample/<user>/status/<tweet>
RewriteCond %{REQUEST_METHOD} =DELETE
RewriteRule ^/([^/]+)/status/([^/]+)$ /tweet_del.php?user=$1&id=$2 [L]
                        

Bootstrapping

PHP has a (little known?) configuration/feature: auto_prepend_file & auto_append_file

"Twitter-like"'s bootstrap

Apache config:

php_value auto_prepend_file ./../bootstrap.php

bootstrap.php:

// PHP configuration?
//require "php_config.php";

// Let's PSR-4!
require "autoloader.php";

// Define an error handler?
//require "error_handler.php";

// Handle authentication?
//require "authentication.php";

// What you want
//require "what_you_want.php";

autoloader.php:

spl_autoload_register(
    function ($className)
    {
        if (stream_resolve_include_path(
            $file = (
                "src/" . str_replace("\\", "/", $className) . ".php")
            )
        ) {
            include $file;
        }
    }
);
$ tree src/
src/
├── Entity
│   ├── Tweet.php
│   └── User.php
├── Negotiation -> ../vendor/willdurand/negotiation/src/Negotiation/
├── Service
│   ├── TweetsService.php
│   └── UsersService.php
└── Views
    ├── Layout.php
    ├── Tweets
    │   ├── Listing.php
    │   └── Page.php
    └── Users
        └── Listing.php

Dependency Injection

Let's leverage two features of PHP:

  • require
  • return statement from included files

Give me the UsersService please!

$usersService = require "dic/users.php";

dic/users.php

return new Service\UsersService(
    require "config/db-connection.php"
);

A more complex DI example

$deliveryGateway = require "dic/service/delivery-gateway.php";

dic/service/delivery-gateway.php

<?php
/**
 * @return Service\Healthcare\Invoicing\HealthcareDeliveryGatewayInterface
 */
return new Service\Healthcare\Invoicing\CachingHealthcareDeliveryGateway(
    new Service\Healthcare\Invoicing\WSHealthcareDeliveryGateway(
        require "dic/service/idfformatter.php",
        require "config/ws-invoicing-hosturi.php",
        require "dic/service/healthcare.beneficiary.php"
    ),
    new Caching()
);

Controllers

Grouped under a common directory, those are the only PHP files that can be reached directly.

$ tree web
web
├── css
│   └── ...
├── img
│   └── ...
├── index.php
├── tweet.php
└── user.php

web/index.php

$lastJoinedUsers = (require "dic/users.php")->getLastJoined();

(new Views\Layout(
    "Twitter - Newcomers",
    new Views\Users\Listing($lastJoinedUsers),
    true
))();

web/index.php

$lastJoinedUsers = (require "dic/users.php")->getLastJoined();

// Serve the right content based on the "Accept" HTTP header
switch (require "dic/negotiated_format.php") {
    case "text/html":
        (new Views\Layout(
            "Twitter - Newcomers",
            new Views\Users\Listing($lastJoinedUsers),
            true
        ))();
        return;
    case "application/json":
        header("Content-Type: application/json");
        echo json_encode($lastJoinedUsers);
        return;
}
http_response_code(406);
                        

Some OO swag

web/index.php

(require "dic/controller.homepage.php")();

dic/controller.homepage.php

return new \Controller\HomePage(
    require "dic/negotiated_format.php",
    require "dic/users.php"
);

\Controller\HomePage

namespace Controller;
use ...;

class HomePage {
    protected $mimeType;
    protected $usersService;

    public function __construct(string $mimeType, UsersService $usersService) {
        $this->mimeType = $mimeType;
        $this->usersService = $usersService;
    }

    public function __invoke(): void {
        $lastJoinedUsers = $this->usersService->getLastJoined();
        switch ($this->mimeType) {
            case "text/html":
                (new Layout(
                    "Twitter - Newcomers", new Listing($lastJoinedUsers), true
                ))();
                return;
            case "application/json":
                header("Content-Type: application/json");
                echo json_encode($lastJoinedUsers);
                return;
        }
        http_response_code(406);
    }
}

Templating

  • require() PHP/html files
  • design your own template function helpers (header();footer();menu();)
  • PHP class & __invoke()
  • Use a dedicated library?
namespace Views;

class Layout {
    protected $title;
    protected $body;

    public function __construct(string $title, callable $body) {
        $this->title = $title;
        $this->body = $body;
    }

    public function __invoke() {
?>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
  <title><?= htmlspecialchars($this->title) ?></title>
</head>
<body>

<div class="container">
    <div class="row">
        <?= ($this->body)(); ?>
    </div>
</div>
</body>
</html>
<?php
    }
}

Benchmark?

LTS?

Lines I had to change recently in a PHP 5.1 application made in 2009 to work on a shiny new box with PHP 7.1:

     <Directory /var/www/>
-        Order Allow,Deny
-        Allow from all
+        Require all granted
+        AllowOverride all
     </Directory>
 </VirtualHost>

Conclusions

  • Re-usability of code is achieved with libraries
  • Re-usability of concepts is achieved by following patterns
  • Today's PHP frameworks are not frameworks anymore, they are pre-glued loosy coupled components meant to be replaceable
  • "Less is more": Refactor your "framework-based" application to use the most suitable components that fits your needs. You will gain control, performance and maintenance
It seems that perfection is attained not when there is nothing more to add, but when there is nothing more to remove.

Antoine de Saint Exupéry

Questions?

Thanks

"Sample PHP 7.1 application" available at: https://github.com/patrickallaert/php-sample-application

Don't forget to rate this talk on https://joind.in/talk/18127

Stay in touch!

@patrick_allaert

patrickallaert@php.net