Building a PHP
I am a
If you wish to make apple pie tomato sauce from scratch,
you must first create the universe
Carl Sagan
Conor Smith
Step-by-step through the application building process
How I structure my PHP applications
How I make my code interact with third-party packages
Requirements
Public-facing website
Cron that polls PHP Dublin
Administration?
https://github.com/conorsmith/phpdublinmonitor
{
"name": "conorsmith/phpdublinmonitor",
"autoload": {
"psr-4": {
"ConorSmith\\PhpDublinMonitor\\": "src/"
}
}
}
/composer.json
/vendor
/.gitignore
<?php
declare(strict_types=1);
require_once __DIR__ . "/bootstrap.php";
/console
<?php
declare(strict_types=1);
require_once __DIR__ . "/vendor/autoload.php";
/bootstrap.php
$ composer require symfony/console
namespace ConorSmith\PhpDublinMonitor\Console;
use Symfony\Component\Console\Application;
class Kernel
{
public function handle(): void
{
$delegateKernel = new Application;
$delegateKernel->run();
}
}
/src/Console/Kernel.php
require_once __DIR__ . "/bootstrap.php";
(new ConorSmith\PhpDublinMonitor\Console\Kernel)->handle();
/console
$ php console
Console Tool
Usage:
command [options] [arguments]
Options:
-h, --help Display this help message
-q, --quiet Do not output any message
-V, --version Display this application version
--ansi Force ANSI output
--no-ansi Disable ANSI output
-n, --no-interaction Do not ask any interactive question
-v|vv|vvv, --verbose Increase the verbosity of messages: 1 for normal output,
2 for more verbose output and 3 for debug
Available commands:
help Displays help for a command
list Lists commands
src
Console
Kernel.php
vendor
[...]
.gitignore
bootstrap.php
composer.json
composer.lock
console
class Kernel
{
/** @var Application */
private $delegateKernel;
public function __construct(Application $delegateKernel)
{
$this->delegateKernel = $delegateKernel;
}
public function handle(): void
{
$delegateKernel->run();
}
}
/src/Console/Kernel.php
(new ConorSmith\PhpDublinMonitor\Console\Kernel(
new Symfony\Component\Console\Application
))
->handle();
/console
namespace ConorSmith\PhpDublinMonitor\Console;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class LogWebsiteStatusAction extends Command
{
protected function configure()
{
$this
->setName("log:status")
->setDescription(
"Logs the current status of the PHP Dublin website"
);
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$output->writeln("placeholder text");
}
}
/src/Console/LogWebsiteStatusAction.php
$symfonyKernel = new Symfony\Component\Console\Application;
$symfonyKernel->add(
new ConorSmith\PhpDublinMonitor\Console\LogWebsiteStatusAction
);
(new ConorSmith\PhpDublinMonitor\Console\Kernel(
$symfonyKernel
))
->handle();
/console
$ php console log:status
placeholder text
namespace ConorSmith\PhpDublinMonitor;
use Illuminate\Contracts\Container\Container;
class Application
{
/** @var Container */
private $container;
public function __construct(Container $container)
{
$this->container = $container;
$this->registerServices();
}
private function registerServices(): void
{
//
}
public function make(string $className)
{
return $this->container->make($className);
}
}
/src/Application.php
use ConorSmith\PhpDublinMonitor\Console\Kernel;
use ConorSmith\PhpDublinMonitor\Console\LogWebsiteStatusAction;
use Symfony\Component\Console\Application as SymfonyConsole;
class Application
{
...
private function registerServices(): void
{
$this->container[Kernel::class] = function ($container) {
$symfonyKernel = new SymfonyConsole;
$symfonyKernel->add(new LogWebsiteStatusAction);
return new Kernel($symfonyKernel);
};
}
...
}
/src/Application.php
require_once __DIR__ . "/vendor/autoload.php";
return new ConorSmith\PhpDublinMonitor\Application(
new Illuminate\Container\Container
);
/bootstrap.php
$application = require_once __DIR__ . "/bootstrap.php";
$kernel = $application->make(
ConorSmith\PhpDublinMonitor\Console\Kernel::class
);
$kernel->handle();
/console
use Doctrine\DBAL\Connection;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class LogWebsiteStatusAction extends Command
{
/** @var Connection */
private $db;
public function __construct(Connection $db)
{
$this->db = $db;
parent::__construct();
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$this->db->insert("website_status", [
'online' => true,
]);
$output->writeln("Dummy data inserted");
}
...
}
/src/Console/LogWebsiteStatusAction.php
use ConorSmith\PhpDublinMonitor\Console\LogWebsiteStatusAction;
use Doctrine\DBAL\DriverManager;
class Application
{
private function registerServices(): void
{
$this->container[Kernel::class] = function ($container) {
...
$symfonyKernel->add($container[LogWebsiteStatusAction::class]);
...
};
$this->container[LogWebsiteStatusAction::class] = function () {
return new LogWebsiteStatusAction(
DriverManager::getConnection([
'driver' => "pdo_mysql",
'host' => getenv('DB_HOST'),
'dbname' => getenv('DB_NAME'),
'user' => getenv('DB_USER'),
'password' => getenv('DB_PASS'),
])
);
};
}
}
/src/Application.php
DB_HOST=localhost
DB_NAME=phpdublinmonitor
DB_USER=phpdublinmonitor
DB_PASS=password
/.env
/vendor
.env
/.gitignore
DB_HOST=
DB_NAME=
DB_USER=
DB_PASS=
/.env.example
use Dotenv\Dotenv;
class Application
{
public function __construct(Container $container)
{
$this->container = $container;
$this->boot();
}
private function boot(): void
{
(new Dotenv(__DIR__ . "/../"))->load();
$this->registerServices();
}
...
}
/src/Application.php
<?php
declare(strict_types=1);
require_once __DIR__ . "/vendor/autoload.php";
(new \Dotenv\Dotenv(__DIR__))->load();
return [
'paths' => [
'migrations' => __DIR__ . "/database/migrations",
],
'environments' => [
'default_database' => "env_vars",
'env_vars' => [
'adapter' => "mysql",
'host' => getenv('DB_HOST'),
'name' => getenv('DB_NAME'),
'user' => getenv('DB_USER'),
'pass' => getenv('DB_PASS'),
],
],
];
/phinx.php
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
class CreateWebsiteStatusTable extends AbstractMigration
{
public function up()
{
$this->table('website_status')
->addColumn('online', "boolean")
->save();
}
}
/database/migrations/..._created_website_status_table.php
use Doctrine\DBAL\Connection;
use Icecave\Chrono\Clock\ClockInterface;
class LogWebsiteStatusAction extends Command
{
/** @var ClockInterface */
private $clock;
public function __construct(Connection $db, ClockInterface $clock)
{
$this->db = $db;
$this->clock = $clock;
parent::__construct();
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$this->db->insert("website_status", [
'online' => true,
'logged_at' => $this->clock->utcDateTime()->format("Y-m-d H:i:s"),
]);
...
}
...
}
/src/Console/LogWebsiteStatusAction.php
public function up()
{
$this->table('website_status')
->addColumn('logged_at', "datetime")
->save();
}
/database/migrations/..._add_logged_at_to_website_status_table.php
use Icecave\Chrono\Clock\SystemClock;
class Application
{
private function registerServices(): void
{
$container[LogWebsiteStatusAction::class] = function ($container) {
return new LogWebsiteStatusAction(
DriverManager::getConnection([...]),
new SystemClock
);
};
}
}
/src/Application.php
$ php console log:status
Dummy data inserted
mysql> select * from website_status;
+----+--------+---------------------+
| id | online | logged_at |
+----+--------+---------------------+
| 1 | 1 | 2017-12-08 16:27:25 |
+----+--------+---------------------+
1 row in set (0.00 sec)
database
migrations
..._create_website_status_table.php
..._add_logged_at_to_website_status_table.php
src
Console
Kernel.php
LogWebsiteStatusAction.php
Application.php
vendor
...
.env
.env.example
.gitignore
bootstrap.php
composer.json
composer.lock
console
phinx.php
class Application
{
private function boot(): void
{
...
$this->registerServiceProviders();
}
private function registerServiceProviders(): void
{
(new Console\ServiceProvider)->register($this->container);
}
}
/src/Application.php
use Illuminate\Contracts\Container\Container;
class ServiceProvider
{
public function register(Container $container)
{
...
}
}
/src/Console/ServiceProvider.php
use Doctrine\DBAL\Connection;
use Icecave\Chrono\Clock\ClockInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
/src/Console/LogWebsiteStatusAction.php
namespace ConorSmith\PhpDublinMonitor;
use Doctrine\DBAL\Connection;
use Icecave\Chrono\Clock\ClockInterface;
class LogWebsiteStatus
{
/** @var Connection */
private $db;
/** @var ClockInterface */
private $clock;
public function __construct(Connection $db, ClockInterface $clock)
{
$this->db = $db;
$this->clock = $clock;
}
public function __invoke(): void
{
$this->db->insert("website_status", [
'online' => true,
'logged_at' => $this->clock->utcDateTime()->format("Y-m-d H:i:s"),
]);
}
}
/src/LogWebsiteStatus.php
namespace ConorSmith\PhpDublinMonitor\Console;
use ConorSmith\PhpDublinMonitor\LogWebsiteStatus;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class LogWebsiteStatusAction extends Command
{
/** @var LogWebsiteStatus */
private $logWebsiteStatus;
public function __construct(LogWebsiteStatus $logWebsiteStatus)
{
$this->logWebsiteStatus = $logWebsiteStatus;
parent::__construct();
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$this->logWebsiteStatus->__invoke();
$output->writeln("Dummy data inserted");
}
}
/src/Console/LogWebsiteStatusAction.php
private function registerServiceProviders(): void
{
(new Console\ServiceProvider)->register($this->container);
(new ServiceProvider)->register($this->container);
}
/src/Application.php
public function register(Container $container): void
{
$container[LogWebsiteStatus::class] = function ($container) {
return new LogWebsiteStatus(
DriverManager::getConnection([
'driver' => "pdo_mysql",
'host' => getenv('DB_HOST'),
'dbname' => getenv('DB_NAME'),
'user' => getenv('DB_USER'),
'password' => getenv('DB_PASS'),
]),
new SystemClock
);
};
}
/src/ServiceProvider.php
use GuzzleHttp\Client as Guzzle;
class ServiceProvider
{
public function register(Container $container): void
{
$container[LogWebsiteStatus::class] = function ($container) {
return new LogWebsiteStatus(
new Guzzle([
'base_uri' => "https://phpdublin.com/",
'http_errors' => false,
]),
...
);
};
}
}
/src/ServiceProvider.php
use GuzzleHttp\ClientInterface as Guzzle;
class LogWebsiteStatus
{
/** @var Guzzle */
private $guzzle;
public function __invoke(): void
{
$response = $this->guzzle->request('GET', "/");
$this->db->insert("website_status", [
'online' => $response->getStatusCode() === 200,
...
]);
}
}
/src/LogWebsiteStatus.php
use Teapot\StatusCode;
class LogWebsiteStatus
{
/** @var Guzzle */
private $guzzle;
public function __invoke(): void
{
$response = $this->guzzle->request('GET', "/");
$this->db->insert("website_status", [
'online' => $response->getStatusCode() === StatusCode::OK,
...
]);
}
}
/src/LogWebsiteStatus.php
$ php console log:status
PHP Dublin website status logged
database
migrations
..._create_website_status_table.php
..._add_logged_at_to_website_status_table.php
src
Console
Kernel.php
LogWebsiteStatusAction.php
ServiceProvider.php
Application.php
LogWebsiteStatus.php
ServiceProvider.php
vendor
...
.env
.env.example
.gitignore
bootstrap.php
composer.json
composer.lock
console
phinx.php
<?php
declare(strict_types=1);
echo "placeholder text";
/public/index.php
$application = require_once __DIR__ . "/../bootstrap.php";
$action = $application->make(
ConorSmith\PhpDublinMonitor\Web\DisplayWebsiteStatusAction::class
);
$action();
/public/index.php
namespace ConorSmith\PhpDublinMonitor\Web;
class DisplayWebsiteStatusAction
{
public function __invoke(): void
{
echo "placeholder text";
}
}
/src/Web/DisplayWebsiteStatusAction.php
use Doctrine\DBAL\Connection;
class DisplayWebsiteStatusAction
{
/** @var Connection */
private $db;
public function __invoke(): void
{
$row = $this->db->fetchAssoc(
"SELECT * FROM website_status ORDER BY logged_at DESC LIMIT 1"
);
echo sprintf(
"The PHP Dublin website is <strong>%s</strong>",
$row['online'] ? "online" : "offline"
);
}
}
/src/Web/DisplayWebsiteStatusAction.php
class ServiceProvider
{
public function register(Container $container): void
{
$container[DisplayWebsiteStatusAction::class] = function ($container) {
return new DisplayWebsiteStatusAction(
DriverManager::getConnection([
'driver' => "pdo_mysql",
'host' => getenv('DB_HOST'),
'dbname' => getenv('DB_NAME'),
'user' => getenv('DB_USER'),
'password' => getenv('DB_PASS'),
])
);
};
}
}
/src/Web/ServiceProvider.php
private function registerServiceProviders(): void
{
...
(new Web\ServiceProvider)->register($this->container);
}
/src/Application.php
public function register(Container $container): void
{
$container[LogWebsiteStatus::class] = function ($container) {
return new LogWebsiteStatus(
...
$container[Connection::class]
);
};
$container->singleton(Connection::class, function ($container) {
return DriverManager::getConnection([
'driver' => "pdo_mysql",
'host' => getenv('DB_HOST'),
'dbname' => getenv('DB_NAME'),
'user' => getenv('DB_USER'),
'password' => getenv('DB_PASS'),
]);
});
}
/src/ServiceProvider.php
use League\Plates\Engine;
class DisplayWebsiteStatusAction
{
/** @var Engine */
private $templateEngine;
public function __invoke(): void
{
...
echo $this->templateEngine->render("home", [
'status' => $row['online'] ? "online" : "offline",
]);
}
}
/src/Web/DisplayWebsiteStatusAction.php
$container[DisplayWebsiteStatusAction::class] = function ($container) {
return new DisplayWebsiteStatusAction(
...
new Engine(__DIR__ . "/../../resources/templates")
);
};
/src/Web/ServiceProvider.php
<div class="container">
<p class="text-center" style="margin-top: 20px;">
The PHP Dublin website is <strong><?=$status?></strong>
</p>
</div>
/resources/templates/home.php
echo $this->templateEngine->render("home", [
...
'lastUpdated' => DateTimeImmutable::createFromFormat(
"Y-m-d H:i:s",
$row['logged_at']
),
]);
/src/Web/DisplayWebsiteStatusAction.php
namespace ConorSmith\PhpDublinMonitor\Web;
use League\Route\RouteCollection;
use Zend\Diactoros\Response;
use Zend\Diactoros\ServerRequestFactory;
class Kernel
{
/** @var RouteCollection */
private $routes;
public function handle(): void
{
$this->routes->dispatch(
ServerRequestFactory::fromGlobals(),
new Response
);
}
}
/src/Web/Kernel.php
$container[RouteCollection::class] = function ($container) {
$routes = new RouteCollection;
$routes->map('GET', "/", $container[DisplayWebsiteStatusAction::class]);
return $routes;
};
/src/Web/ServiceProvider.php
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
class DisplayWebsiteStatusAction
{
public function __invoke(
ServerRequestInterface $request,
ResponseInterface $response
): ResponseInterface {
...
return $response;
}
}
/src/Web/DisplayWebsiteStatusAction.php
use Zend\Diactoros\Response\EmitterInterface;
class Kernel
{
/** @var EmitterInterface */
private $emitter;
public function handle(): void
{
$response = $this->routes->dispatch(...);
$this->emitter->emit($response);
}
}
/src/Web/Kernel.php
public function register(Container $container): void
{
$container->bind(EmitterInterface::class, SapiEmitter::class);
}
/src/Web/ServiceProvider.php
use Zend\Diactoros\Response\HtmlResponse;
class DisplayWebsiteStatusAction
{
public function __invoke(
ServerRequestInterface $request,
ResponseInterface $response
): ResponseInterface {
...
return new HtmlResponse(
$this->templateEngine->render(...)
);
}
}
/src/Web/DisplayWebsiteStatusAction.php
class DisplayHistoricStatusesAction
{
public function __invoke(
ServerRequestInterface $request,
ResponseInterface $response
): ResponseInterface {
$rows = $this->db->fetchAll(
"SELECT * FROM website_status ORDER BY logged_at DESC LIMIT 10"
);
return new HtmlResponse(
$this->templateEngine->render("historic", [
'logEntries' => array_map(function (array $row) {
return [
'status' => $row['online'] ? "online" : "offline",
'lastUpdated' => DateTimeImmutable::createFromFormat(
"Y-m-d H:i:s",
$row['logged_at']
),
];
}, $rows),
])
);
}
}
/src/Web/DisplayHistoricStatusesAction.php
$container[RouteCollection::class] = function ($container) {
$routes = new RouteCollection;
$routes->map(
'GET',
"/",
$container[DisplayWebsiteStatusAction::class]
);
$routes->map(
'GET',
"/historic",
$container[DisplayHistoricStatusesAction::class]
);
return $routes;
};
/src/Web/ServiceProvider.php
$container[HttpBasicAuthentication::class] = function ($container) {
return new HttpBasicAuthentication([
'secure' => false,
'users' => [
getenv('ADMIN_USER') => getenv('ADMIN_PASS'),
],
]);
};
$container[RouteCollection::class] = function ($container) {
...
$routes->map(
'GET',
"/historic",
$container[DisplayHistoricStatusesAction::class]
)
->middleware($container[HttpBasicAuthentication::class]);
...
};
/src/Web/ServiceProvider.php
$container[RouteCollection::class] = function ($container) {
$routes = new RouteCollection;
include __DIR__ . "/../../routes.php";
return $routes;
};
/src/Web/ServiceProvider.php
namespace ConorSmith\PhpDublinMonitor\Web;
$routes->map(
'GET',
"/",
$container[DisplayWebsiteStatusAction::class]
);
$routes->map(
'GET',
"/historic",
$container[DisplayHistoricStatusesAction::class]
)
->middleware($container[HttpBasicAuthentication::class]);
/routes.php
{
"name": "conorsmith/phpdublinmonitor",
"autoload": {
"psr-4": {
"ConorSmith\\PhpDublinMonitor\\": "src/"
}
},
"require": {
"symfony/console": "^3.3",
"illuminate/container": "^5.5",
"doctrine/dbal": "^2.6",
"vlucas/phpdotenv": "^2.4",
"robmorgan/phinx": "^0.9.1",
"icecave/chrono": "^1.0",
"guzzlehttp/guzzle": "^6.3",
"shrikeh/teapot": "^2.3",
"league/plates": "^3.3",
"league/route": "^3.0",
"zendframework/zend-diactoros": "^1.6",
"tuupola/slim-basic-auth": "^2.3"
}
}
https://github.com/conorsmith/phpdublinmonitor
No framework
Remember the code is here:
Serves web pages
Queries a URL
Interacts with DB
Loads env vars
Manages migrations
PSR-7 compatible