What Does A Modern PHP Application Look Like?
php[tek] May 16, 2023
Tim Bond
Who am I?
- Senior Software Engineer
- Frontend developer
(when I have to be) - Seattle is my home
- Cyclocross racer

Opinions ahead

<!--include /text/header.html-->
<!--getenv HTTP_USER_AGENT-->
<!--ifsubstr $exec_result Mozilla-->
Hey, you are using Netscape!<p>
<!--endif-->
<!--sql database select * from table where user='$username'-->
<!--ifless $numentries 1-->
Sorry, that record does not exist<p>
<!--endif exit-->
Welcome <!--$user-->!<p>
You have <!--$index:0--> credits left in your account.<p>
<!--include /text/footer.html-->PHP/FI Code

<?php
require 'header.php';
?>
<!-- main page layout goes here -->
<?php
require 'footer.php';
?>index.php

<h1>Welcome</h1>
<?php
include 'greeting.php';
?>
<p>Goodbye!</p>include() and require()
<?php
echo 'Hello world!';
?>index.php resume.php guestbook.php includes/ ├── header.php ├── footer.php └── functions.php
index.php
resume.php
guestbook.php
includes/
├── header.php
├── footer.php
└── functions.php
lib/
└── adodb
├── (more files)
└── adodb.inc.php
<?php
require_once 'includes/functions.php';
require_once 'lib/adodb/adodb.inc.php';
// database setup
require 'header.php';
?>
<!-- guestbook page layout goes here -->
<?php
require 'footer.php';
?>guestbook.php
index.php
resume.php
guestbook/
├── index.php
├── view.php
└── submit.php
includes/
├── header.php
├── footer.php
├── database.php
└── functions.php
lib/
└── adodb
└── adodb.inc.php<?php
require '../includes/database.php';
require_once '../includes/functions.php';
// code to generate guestbookguestbook/index.php
🤯
reset();
Goals
PHP 5.0 (2004)
- New* OOP
- Less focus on procedural code with global functions
PHP 5.1 (2005)
Autoloading
<?php
require 'path/to/MyClass.php';
new MyClass();🚫
<?php
spl_autoload_register(function ($class) {
require 'classes/' . $class . '.class.php';
});<?php
new MyClass();PHP 5.3 (2009)
Namespaces
Old:
class Acme_Logger {
}
New:
namespace Acme;
class Logger {
}
Fatal error: Cannot declare class Logger, because the name is already in use
Namespaces
<?php
namespace Acme;
spl_autoload_register(function ($class) {
include 'classes/' . $class . '.class.php';
});<?php
use Acme\Logger;
use Zend\Controller;
// include autoloader files
new Logger();
new Controller();✅ Loading classes
❓ Managing classes

Packages (2004)
- Authentication
- Benchmarking
- Caching
- Configuration
- Console
- Database
- Date and Time
- Encryption
- File Formats
- File System
- HTML
- HTTP
- Images
- Internationalization
- Logging
- Math
- Networking
- Numbers
- Payment
- PEAR
- PHP
- Science
- Streams
- System
- Text
- Tools and Utilities
- XML
- Web Services
Today: 33 packages

2012
Composer
- Dependency manager
-
composer require guzzlehttp/guzzle
-
- Dependency solver
- Gateway to Packagist
Autoloading with Composer
{
"autoload": {
"psr-4": {"Acme\\": "src/"}
}
}<?php
require __DIR__ . '/vendor/autoload.php';
$obj = new Acme\MyClass();

<?php
namespace Psr\Log;
interface LoggerInterface {
public function emergency(string|\Stringable $message, array $context = []) : void;
public function alert(string|\Stringable $message, array $context = []) : void;
public function critical(string|\Stringable $message, array $context = []) : void;
public function error(string|\Stringable $message, array $context = []) : void;
public function warning(string|\Stringable $message, array $context = []) : void;
public function notice(string|\Stringable $message, array $context = []) : void;
public function info(string|\Stringable $message, array $context = []) : void;
public function debug(string|\Stringable $message, array $context = []) : void;
public function log($level, string|\Stringable $message, array $context = []) : void;
}<?php
namespace Acme\SDK;
class AcmeHttpClient {
private LoggerInterface $logger;
public function __construct(LoggerInterface $logger) {
$this->logger = $logger;
}
public function doSomething() {
$this->logger->debug('Started doing something');
}
}✅ Loading files without include
✅ Loading 3rd party code
✅ Code plays well with others
❓ Structure our own app
3 Kinds of Apps
- Libraries
- e.g. Guzzle, Monolog
- Full Stack apps
- APIs
index.php
resume.php
guestbook/
├── index.php
├── view.php
└── submit.php
includes/
├── header.php
├── footer.php
├── database.php
└── functions.php
lib/
└── adodb
└── adodb.inc.php- 📁 src/
- all PHP code needed to run the project
- 📁 public/
- Point the webserver here
- 📁 tests/
- Unit, Integration, System
- 📁 vendor/
- Never checked in to version control!
- 📁 docs/
- 📁 bin/
- 📁 config/
- 📁 templates/
- 📄 composer.json
- 📄 composer.lock
- 📄 README(.*)
- 📄 CONTRIBUTING(.*)
- 📄 CHANGELOG(.*)
- 📄 INSTALLING(.*)
- 📄 LICENSE(.*)
src
├── Controller
│ └── UserController
├── Entity
│ └── User
├── Repository
│ └── UserRepository
└── Service
└── UserServiceGroup by type
src
├── User
│ ├── User
│ ├── UserController
│ └── UserService
└── Product
├── Product
├── ProductController
└── ProductServiceGroup by feature
src
├── App
│ ├── ConfigService
│ ├── Logger
└── Domain
├── User
│ └── UserService
└── ProductHybrid grouping
Type
vs.
Feature
vs.
Hybrid

Controllers?
Services?
Repositories?
Entities?
Request
Controller
Service
Repository
Entity
Response
GET /api/users/123
Router
Router
- Webserver points all traffic to
public/index.php - Framework bootstraps itself
- Route file is invoked to determine which class/method to call
<?php
$router->get('/api/users/{id}', UserController::getUserById);
$router->post('/api/users/{id}', UserController::updateUserById);Router
$app->group('/api', function (RouteCollectorProxy $api) {
$api->group('/users/{id}', function (RouteCollectorProxy $users) {
$users->patch('', UserController::UpdateUser);
$users->post('/reset-password', UsersController::ResetPassword);
});
});$router->patch('/api/users/{id}/', UserController::UpdateUser);
$router->post('/api/users/{id}/reset-password', UsersController::ResetPassword);🚫
{
-
Accepts a request
-
Rejects obviously invalid requests
-
ZERO validation, ZERO business logic
-
Passes off to service
-
later...
-
Receives response from service, formats for display
Controller
<?php
namespace Acme\Controller;
class UserController {
// maps to GET /api/users/{id}
public function getUserById(string $id) {
if(empty($id)) {
throw new BadRequestException("ID is required");
}
$user = $this->userService->getById($id);
$this->view->render($user);
}
}-
UserController
-
getUserById
-
createUser
-
updateUser
-
-
AbstractUserAction
-
GetUserAction
-
CreateUserAction
-
UpdateUserAction
-
Controllers vs Actions
-
Validation lives here
-
Business logic lives here
-
Talks to repositories, APIs
Service
<?php
namespace Acme\Service;
class UserService {
public function getUserById(string $id) {
if(!is_numeric($id)) {
throw new DomainException("ID must be a number");
}
if($id < 1) {
throw new DomainException("ID must be a positive number");
}
return $this->userRepository->getById((int)$id);
}
}-
Talks to the data storage
-
Only accessed from services--never controllers
-
Treat incoming data as sanitized
-
"Hydrates" entities to give back to the service
Repository
<?php
namespace Acme\Repository;
class UserRepository {
public function getUserById(int $id) : User {
$sql = 'SELECT * FROM users WHERE id = ? LIMIT 1';
$result = $this->connection->execute_query($sql, [$id]);
$row = $result->fetch_object();
return new User($row->id, $row->name);
}
}-
"an object that has an identity, which is independent of the changes of its attributes"
-
Dumb "containers" for data--no functionality
Entity
<?php
namespace Acme;
class User {
public function __construct(
public readonly int $id,
public readonly string $name,
) {
}
}-
"an object that has an identity, which is independent of the changes of its attributes"
-
Dumb "containers" for data--no functionality
-
Cannot be created in an invalid state
Entity
<?php
new User(1, 'Me');
new User(null, 'Nobody');🚫
-
Data mapper
-
Clear definition of boundaries
-
Each class does one thing
-
-
Active record
-
One class does everything
-
User::create(...), User::getById(...), User::setActive()
-
-
Easy to leak logic into
-
Active Record vs
Data Mapper
Pass arrays around
Do
Create a value object
Don't
<?php
namespace Acme\Controller;
class UserController {
public function createUser() {
$this->userService->createUser($_GET);
}
}Don't
<?php
namespace Acme\Controller;
class UserController {
public function createUser(RequestInterface $request) {
$params = $request->getQueryParams();
$this->userService->createUser(
$params['firstName'],
$params['lastName']
);
}
}Do
PSR-7
✅ Fully unit testable
<?php
namespace Acme\Controller;
class OrderService {
public function createOrder(...) {
$user = $this->userRepository->findOrCreate(...);
$order = $this->orderRepository->create(...);
return new NewOrderDTO($user, $order);
}
}On the return path
<?php
namespace Acme\DTO;
class NewOrderDTO {
public function __construct(
public readonly User $user,
public readonly Order $order,
) {
}
}// in the controller
$dto = $service->createOrder(...);
$dto->user->name;
$dto->order->number;-
Unit tests
-
Run entirely offline, connect to nothing
-
"Fake" responses from databases, APIs
-
#1 way to catch regression bugs
-
-
Integration tests
-
Test the code in a live enviornment
-
-
E2E tests
-
Automated "manual testing"
-
Tests
| Unit | Integration | E2E | |
|---|---|---|---|
| Library | ✅ | ❌ | ❌ |
| Full Stack | ✅ | 👍️ | 👍️ |
| API | ✅ | 👍️ | 🤔 |
Software's job is to get stuff done
The deliverable is the product, not the code
There is no “right” architecture...
This isn’t an excuse to never make things better, but instead a way to give you perspective. Worry less about elegance and perfection; instead strive for continuous improvement and creating a livable system that your team enjoys working in and sustainably delivers value.
—Justin Etheredge
Today 11:00 Database Abstractions and Where They Leak
Today 2:00 Domain-driven Design in PHP Workshop
Tomorrow 10:00 Dependency Injection for Mere Humans
Tomorrow 2:00 Leveling Up with Unit Testing
Thursday 11:00 Immutability to Save an Ever-Changing World
Thursday 3:00 Building a SOLID Foundation
Thursday 3:00 Building Enterprise Applications with Domain Driven Design (DDD)
Resources
phptherightway.com
github.com/php-pds/skeleton
Questions
What Does A Modern PHP Application Look Like?
By Tim Bond
What Does A Modern PHP Application Look Like?
php[tek] May 16, 2023
- 563