php[tek] May 16, 2023
Tim Bond
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';
?><h1>Welcome</h1>
<?php
include 'greeting.php';
?>
<p>Goodbye!</p><?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';
?>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 guestbook🤯
reset();
Autoloading
<?php
require 'path/to/MyClass.php';
new MyClass();🚫
<?php
spl_autoload_register(function ($class) {
require 'classes/' . $class . '.class.php';
});<?php
new MyClass();Namespaces
Old:
class Acme_Logger {
}
New:
namespace Acme;
class Logger {
}
Fatal error: Cannot declare class Logger, because the name is already in use
<?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
Today: 33 packages
composer require guzzlehttp/guzzle
{
"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
index.php
resume.php
guestbook/
├── index.php
├── view.php
└── submit.php
includes/
├── header.php
├── footer.php
├── database.php
└── functions.php
lib/
└── adodb
└── adodb.inc.phpsrc
├── Controller
│ └── UserController
├── Entity
│ └── User
├── Repository
│ └── UserRepository
└── Service
└── UserServicesrc
├── User
│ ├── User
│ ├── UserController
│ └── UserService
└── Product
├── Product
├── ProductController
└── ProductServicesrc
├── App
│ ├── ConfigService
│ ├── Logger
└── Domain
├── User
│ └── UserService
└── ProductType
vs.
Feature
vs.
Hybrid
Request
Controller
Service
Repository
Entity
Response
GET /api/users/123
Router
public/index.php
<?php
$router->get('/api/users/{id}', UserController::getUserById);
$router->post('/api/users/{id}', UserController::updateUserById);$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
<?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
Validation lives here
Business logic lives here
Talks to repositories, APIs
<?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
<?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
<?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
<?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
Pass arrays around
Create a value object
<?php
namespace Acme\Controller;
class UserController {
public function createUser() {
$this->userService->createUser($_GET);
}
}<?php
namespace Acme\Controller;
class UserController {
public function createUser(RequestInterface $request) {
$params = $request->getQueryParams();
$this->userService->createUser(
$params['firstName'],
$params['lastName']
);
}
}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);
}
}<?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"
| 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)
phptherightway.com
github.com/php-pds/skeleton