Stefan

Welcome to another insightful edition of The PHP Teacher! This month, we delve into a variety of topics crucial for both the budding and seasoned PHP developer. We explore the practicalities of working with HTTP requests and responses, dissect the complexities of AI and its implications for our industry, and look at ways to structure our code for better maintainability and testability. We also examine tools that can help us build better command-line applications and explore practical strategies for incident response. Whether you are interested in clean code principles, architectural patterns, or harnessing the latest technologies, we have something for you.

This issue features:

  • A deep dive into PSR-7 and the http-tortilla library for handling HTTP requests and responses
  • An examination of the GIMM program and how it prepares students for the tech industry.
  • A critical analysis of Large Language Models (LLMs), their capabilities, limitations and legal issues.
  • A practical look at tabletop exercises for disaster planning.
  • A guide to building console applications using the Minicli framework.
  • An exploration of maze generation using PHP.
  • A discussion on refactoring techniques for improving codebases.
  • An introduction to PSR-20 Clock interface.
  • A comparison of ADR and MVC architectural patterns.
  • A thought-provoking piece on the current fears and possibilities around AI.

We hope these articles will challenge and inspire you on your journey as a PHP developer.

HTTP Tortilla: Wrapping PSR-7 for Developer Convenience**

By Tim Lytle

In the world of PHP development, PSR-7 interfaces for request and response messages provide a valuable standard. They establish a well-defined interface for working with HTTP requests and responses, introducing a reasonable amount of immutability. This results in increased ease when testing and provides confidence and simplicity when integrating with existing libraries that manipulate and extract information from the HTTP life-cycle. However, these interfaces can be minimal, and developers may long for more convenient methods tailored to specific use cases. This article explores the trade-offs and introduces http-tortilla, a library designed to add developer-friendly methods to PSR-7 objects.

The Trade-Off:

While PSR-7 defines a minimum set of methods for inspecting and building responses or requests, this can sometimes be too basic for internal use. For example, $request->getQueryParams() might be sufficient, but $request->getQueryParam($name, $default = null) would be more convenient. Similarly, instead of checking if $member = $request->getAttribute(Member::class) is an instance of the desired member entity, we might prefer $request->isAuthenticated() and $request->getAuthenticatedMember(). These improvements can reduce boilerplate code and make development more efficient.

The Challenge of Custom Requests:

Creating custom request objects that wrap PSR-7 requests can solve these convenience issues, but it presents integration problems. If you need to integrate with library code that expects a PSR-7 request, your custom request must implement all PSR-7 methods. Also, PSR-15’s middleware pattern, which is highly valuable, requires requests to implement PSR-7 interfaces. Extending existing library code might seem like a solution, but it can lead to problems with the with*() methods, testing, and coupling to code that may change.

The Solution: http-tortilla

The best solution is to create your own request object, but one that implements all the PSR-7 methods. The http-tortilla library helps to achieve this by using a trait for wrapping PSR-7 objects. This trait moves all the boilerplate proxy methods into one place, making it easy to see the added methods and modify the behaviour of PSR-7 methods.

trait ServerRequestWrapper
{
    private ServerRequestInterface $wrapped;

    protected function setWrapped(ServerRequestInterface $request): void
    {
        $this->wrapped = $request;
    }

    private function getWrapped(): ServerRequestInterface
    {
        if (!($this->wrapped instanceof ServerRequestInterface)) {
            throw new \UnexpectedValueException('must `setMessage` before using it');
        }

        return $this->wrapped;
    }

    public function getServerParams(): array
    {
        return $this->getWrapped()->getServerParams();
    }
    // ... other proxy methods
}

This trait can be used in a custom ServerRequest class:

class ServerRequest implements ServerRequestInterface
{
    use ServerRequestWrapper;

    public function __construct(ServerRequestInterface $request)
    {
        $this->setWrapped($request);
    }

    public function getQueryParam($name, $default = null): ?string
    {
        return $this->getQueryParams()[$name] ?? $default;
    }
}

Handling Immutability

Because PSR-7 objects are immutable, the with*() methods return a modified version of the wrapped request, not your custom request. To solve this, the trait can be modified to use a wrapping ‘factory’ so that the with*() requests can pass the returned object to the factory for wrapping. This way, evolving the request continues to return an instance of our custom request object.

trait ServerRequestWrapper
{
    private $wrapped;
    private $factory;
    
    protected function setFactory(callable $factory): void {
        $this->factory = $factory;
    }

    private function viaFactory(MessageInterface $message): MessageInterface {
        if (!$this->factory) {
            return $message;
        }
    
    return call_user_func($this->factory, $message);
    }

    public function withCookieParams(array $cookies): ServerRequestInterface {
       return $this->viaFactory(
            $this->getWrapped()->withCookieParams($cookies)
        );
    }
}

Extending Beyond Requests:

These techniques can also be applied to other PSR interfaces, allowing the creation of custom responses and URIs. For example, you can create custom responses like JSON, text or API Problem responses. You can also wrap existing objects, such as exceptions, to act as responses. Similarly, route definitions can be wrapped to act as URIs.

Lazy Responses

One useful concept is the idea of a lazy response, which contains all the information needed to create a response but only generates it when necessary. This could be especially useful in middleware chains, where the response could be modified further up the chain.

class LazyResponse implements ResponseInterface {
    use ResponseWrapper;

    private $factory;
    private $data;

    public function __construct($factory, $data) {
        $this->factory = $factory;
        $this->data = $data;
        $this->setWrapped(new Response());
    }
    
    public function getBody(): StreamInterface {
         $response = call_user_func($this->factory, $this->data)
        return $response->getBody()
    }
}

Conclusion

http-tortilla provides a set of traits that make it easier to create custom PSR-7 objects, adding convenience without sacrificing interoperability. You can install it with composer require phoneburner/http-tortilla and find more examples at github.com/tjlytle/tortilla-example.

     _  _
    ( `  )
   /     \
  |  ---  |
   \     /
    `---'
  HTTP Tortilla

Technology Index: PSR-7, PSR-15, HTTP, Requests, Responses, Middleware, Immutability, Traits, http-tortilla


Life After GIMM: Preparing for the Tech Industry Post-College**

By Adam Giles

The Games, Interactive Media, and Mobile (GIMM) program at Boise State University offers a hands-on approach to technology education, going beyond just game development. It prepares students for the competitive tech market by exposing them to a wide range of emerging technologies and concepts. This article discusses the GIMM program, its goals, and how it prepares students for careers in technology.

What is GIMM?

GIMM is an interdisciplinary program that introduces students to various fields within the tech industry. While it includes game development, it also covers full-stack web development, 3D animation, and AR/VR development. This breadth of experience allows students to build a strong foundation of general knowledge that can be expanded upon.

Hands-On Experience:

The program emphasises hands-on experimentation and the creation of portfolio artifacts. Students are encouraged to work on projects inside and outside the classroom, demonstrating their skills and dedication. These projects are crucial for showing employers that the student has practical experience and is not just relying on theoretical knowledge.

The Generalist vs. Specialist Approach

The GIMM curriculum fosters a generalist mindset, providing a broad understanding of technology. This allows for more flexibility in career choices, with graduates able to take on a variety of roles. However, GIMM doesn’t discourage specialisation. Students can still choose to focus on a particular area, building a deeper understanding in that area while having the general knowledge as well.

Problem-Solving Skills:

One of the key goals of the GIMM program is to develop students’ problem-solving abilities. The tech industry is constantly evolving, so it is crucial to be able to adapt and troubleshoot independently. The curriculum teaches students how to research, understand, and resolve issues effectively. They are taught how to use online resources like Stack Overflow and modify code snippets to fit their specific needs.

Community and Networking:

GIMM also provides a strong sense of community, with opportunities for students to work together on projects, attend events like the GIMM Jam. Alumni also engage with current students via Q&A sessions, offering valuable insights into how they have applied their GIMM knowledge in the tech sector.

Application of Skills:

The GIMM program helps students with career planning. Students create custom portfolio websites and use platforms like LinkedIn to present themselves to employers. The program prioritises securing employment for students, before or soon after graduation.

The Value of a Tech Degree:

With the abundance of online resources, some people may wonder if a tech degree is still necessary. However, programs like GIMM provide a structured path for learning, offering a set curriculum to follow, and allowing students to acquire the foundational knowledge they can expand upon. It also helps students gain practical experience which is essential in the tech sector.

Conclusion

The GIMM program serves as a model for how universities can prepare students for the tech industry. By offering a wide range of experiences, developing problem-solving skills, and emphasising the creation of tangible portfolio artifacts, programs like GIMM equip students with the tools they need to succeed.

     ___
    /   \
   | GIMM |
   \___/
    
  Tech Education

Technology Index: GIMM, Technology Education, Full-Stack Development, AR/VR, Problem-Solving, Portfolio, Generalist, Specialist


CatAIstrophe: Navigating the Fears and Facts of Large Language Models**

By Beth Tucker Long

Recent advancements in Artificial Intelligence (AI), particularly in Large Language Models (LLMs), have sparked a mix of awe and concern. While AI is hailed as revolutionary, there are widespread fears that it could be destructive. This article explores the capabilities and limitations of LLMs and tries to address some of these concerns.

LLMs as Text Prediction Systems

At their core, LLMs are sophisticated text prediction systems. They work by taking a sequence of words (tokens) and predicting the most likely next word. This prediction is based on probability matrices derived from vast amounts of training data. Your phone’s autofill works in a similar way but on a much smaller scale.

Token Size and Training Data

The effectiveness of an LLM depends on several factors, including the amount of training data, the raw processing power, and the token size. Autofill on a mobile phone is limited by token size (amount of existing data considered), and the amount of training data. In contrast, models like GPT-3 and GPT-4 have access to massive datasets and larger token windows, allowing them to consider more text to determine the next token.

How LLMs Work

LLMs like ChatGPT use huge token windows to determine the next token. They also chunk individual words or combine phrases to improve their accuracy. Human trainers also play a role, rating responses and providing manual responses for the AI to learn from. Although the models have additional functionality, they are fundamentally text prediction models.

The Hallucination Problem

The issue is that LLMs generate text based on probability rather than knowledge. This often leads to “hallucinations”, or convincing but ultimately incorrect responses. These are essentially lies that the AI is presenting as fact. For example, if asked about articles written by a specific author, an LLM might generate a list of fake articles and even fake publication dates. The same is true of research papers, which could lead to the spread of disinformation.

AI Agents and Artificial General Intelligence (AGI)

Despite the hype around AI agents, these tools are not yet capable of achieving Artificial General Intelligence (AGI), which is the ability to replicate an intellectual task like a human. Tools like AutoGPT, which claim to perform research and create content, often get stuck in loops and fail to produce useful output. It is safe to say these tools are not very useful for the general public as of yet.

Legal and Ethical Issues

Many LLMs are not open-source, despite their names suggesting otherwise. This raises concerns about where they obtain their training data. For example, Microsoft’s Copilot has been accused of using copyrighted code in its training data. There is also concern around where training data comes from, much of which comes from sources like Common Crawl, which archives the internet including copyrighted material. While this is considered fair use, the legal landscape remains uncertain.

Where AI Can Help

Despite these limitations, AI tools can still be valuable. They can help with rubber ducking, prompting new ideas and approaches to problems. LLMs are also useful for dealing with boilerplate code, summarising large amounts of text, and generating outlines for various tasks.

AI Will Not Replace You

Current AI lacks the intelligence that humans possess. AI cannot learn from mistakes and successes like human brains. While AI may weed out developers who are not strong problem solvers, it cannot replace those who can adapt to new challenges.

     _
    / \
   ( o o )
   \  ~  /
    -----
  CatAIstrophe

Technology Index: Artificial Intelligence, LLM, Large Language Models, GPT, Token, Training Data, Hallucination, Artificial General Intelligence, AutoGPT, Open Source, Copyright, Copilot.


Tabletop: Planning for Disaster**

By Eric Mann

In the world of cybersecurity, being proactive is as important as being reactive. One excellent method of preparing for potential incidents is to conduct tabletop exercises. These exercises simulate cybersecurity incidents in a low-pressure environment, allowing teams to practice their responses and identify weaknesses in their processes. This article explores how to run a tabletop exercise effectively.

What is a Tabletop Exercise?

A tabletop exercise is essentially a role-playing game for cybersecurity incident response. It simulates a breach, and the team members work through their response procedures as if it were real. The key is to have representatives from all key stakeholder groups, such as legal, finance, infrastructure, and executive staff. These exercises help teams understand their roles and responsibilities in an incident.

Creating a Believable Scenario

The simulation should include a clear what, who, and when of the event. The scenario should be realistic and relatable. For instance, you can simulate an infrastructure engineer downloading a malicious file, a customer service representative installing a keylogger, or an executive installing malware on a personal device.

Planning the Simulation

Plan the information you will provide during the simulation based on how you expect the team to react to the incident. If their first step would be to audit network access logs, then plan on providing those network logs for the simulation. Also, be prepared to provide extra information to the team if needed.

Guiding the Team

During the exercise, it’s important not to intervene too much. Allow the team to navigate the scenario and make their own decisions. If they go down a rabbit hole, gently guide them back to the task at hand. As the team becomes more familiar with the incident response playbook, they will rely less on your guidance.

Retrospective and Action Items

After the simulation, allow everyone to take a break before engaging in a retrospective phase. Discuss the team’s performance and create a list of action items for further policy or process refinements.

Regular Repetition

Tabletop exercises should be repeated regularly, every 3-6 months, to test different attack vectors and to improve your incident response plan. You can also use real-world incident reports as the basis for your scenarios. Regular repetition strengthens the response plan and the team’s ability to use it.

Related Reading

For further reading, check out these articles by Eric Mann, also in the php[architect] magazine:

    _____
   |     |
  _|     |_
 /  TABLE  \
|   TOP    |
 \_______/
     Incident Planning

Technology Index: Cybersecurity, Incident Response, Tabletop Exercise, Threat Modeling, Simulation, Security, Incident Response Plan


The Workshop: Minicli - A Minimalist Framework for Building CLI Applications**

By Joe Ferguson

Building command-line interfaces (CLIs) with PHP is a common task, but often involves using heavyweight frameworks like Symfony’s Console Component or Laravel’s Commands. Minicli is a minimalist framework for building CLI applications in PHP. This article demonstrates how to use Minicli to create simple and powerful console applications.

Getting Started with Minicli

Minicli requires PHP 8.0 or higher and the ext-readline extension. To create a new application you can start by creating a new folder and installing minicli using composer:

mkdir demo; cd demo
touch example
chmod +x example
composer require minicli/minicli

Then you create an empty file called example and add the following code:

#!/opt/homebrew/bin/php
<?php
if(php_sapi_name() !== 'cli') { exit;}
require __DIR__ . '/vendor/autoload.php';

use Minicli\App;

$app = new App([
    'app_path' => [__DIR__ . '/app/Command'],
    'theme' => '\\Unicorn', 
    'debug' => false,
]);

$app->registerCommand('demo', function () use ($app) {
    $app->getPrinter()
        ->success('Hello php[architect] :D', false);
    $app->getPrinter()
        ->info('Info message with background', true);
    $app->getPrinter()
        ->error('Error Message :(', false);
});

$app->runCommand($argv);

Make sure you update the shebang line to match your PHP install. The first line of the file, the shebang, tells the system that the file needs to be executed using the PHP binary. The code then sets up autoloading, imports Minicli classes and instantiates the application. It creates a demo command that outputs three messages to the console. To run the application type ./example demo into the console.

Structured Application Layout

A better way to structure the application is to use the 'app_path' configuration and use PSR-4 autoloading. You will need to add the following to your composer.json:

"autoload": {
    "psr-4": {
        "App\\": "app/"
    }
}

Then you create your commands in the app/Command directory. You need to regenerate the autoload file by typing composer dump-auto into the console. You can create DefaultController.php inside the app/Command/Demo directory with the following contents:

namespace App\Command\Demo;
use Minicli\Command\CommandController;

class DefaultController extends CommandController
{
    public function handle(): void
    {
        $this->getPrinter()
            ->success('Hello php[architect] :D', false);
        $this->getPrinter()
            ->info('Info message with background', true);
        $this->getPrinter()
            ->error('Error Message :(', false);
    }
}

The output of running ./example demo remains the same as before. The code will return a non-zero error code if an exception is encountered. It also uses try/catch blocks to catch errors, and display them if debug mode is enabled.

try {
    $app->runCommand($argv);
} catch (CommandNotFoundException $exception) {
    $app->getPrinter()->error("Command Not Found.");
    return 1;
} catch (Exception $exception) {
    if ($app->config->debug) {
        $printer = $app->getPrinter();
        $printer->error("An error occurred:");
        $printer->error($exception->getMessage());
    }
    return 1;
}

Sub-Commands

Minicli also supports sub-commands, which is why the controller was named DefaultController.php. This means you can run ./example demo or ./example demo default and see the same output. You can also create a sub command by creating app/Command/Demo/TpsController.php with the following contents:

namespace App\Command\Demo;
use Minicli\Command\CommandController;

class TpsController extends CommandController
{
    public function handle(): void
    {
        $name = "World";
        if ($this->hasParam('name')) {
            $name = $this->getParam('name');
        }
        $this->getPrinter()->display("Hello $name!");
    }
}

This will display Hello World if you just type ./example demo tps and if you type ./example demo tps --name John it will display Hello John!

Handling Parameters and Flags

Command line applications need to handle input and support flags. You can use $this->hasParam() to check if a parameter has been passed. The parameter value can be retrieved with $this->getParam(). You can also check for flags using $this->hasFlag(), such as --shout. Alternatively, you can use $this->getArgs() to get all command-line arguments in order:

$message = "Hello World";
if ($this->hasFlag('shout')) {
    $message = strtoupper($message);
}
$this->getPrinter()->display($message);

You can also access parameters by name instead of array index to improve readability.

Test Flags

To help with debugging you should implement a test flag that prevents the command from writing to the database, creating files or otherwise making changes.

$test = false;
if ($this->hasFlag('test')) {
    $this->getPrinter()
        ->display("Operating in test mode!");
    $test = true;
}
// Process data and need to update a DB
if (!$test) { // if we're not using test flag
    // update database!
}

Conclusion

Minicli is a useful framework for creating console applications because it’s minimalist and it stays out of the way.

    _
   / \
  |   |
  \___/
  MiniCLI

Technology Index: Minicli, CLI, Command-Line Interface, Console Application, Composer, PSR-4, Command Controllers, Sub-Commands, Parameters, Flags.


Maze Rats, Part Two: Generating Random Mazes with PHP**

By Oscar Merida

Last month, we discussed how to represent mazes using a 2D array of integers. In this issue, we explore how to generate random and solvable mazes in PHP using the recursive backtracker algorithm.

Algorithms for Generating Mazes

Many algorithms exist for generating mazes. This article will focus on the recursive backtracker algorithm, which is known for creating long and winding passages. Other algorithms include:

  • Eller’s algorithm: Generates large mazes in linear time by working one row at a time.
  • Wilson’s algorithm: Uses uniform spanning trees to generate mazes.

Rendering the Maze

Before generating the maze, the first step is to render it. We can use the GD library bundled with PHP to create a PNG image of the maze. The code below is the Renderer class that draws the walls of each cell based on the integer representation of each cell:

class Renderer {
    private \GdImage $image;
    private array $cells;
    private int $color1;
    private int $color2;
    private int $lineWidth;
    
    const WEST = 0x1;
    const EAST = 0x2;
    const SOUTH = 0x4;
    const NORTH = 0x8;

    public function __construct(
    private int $height,
    private int $width,
    private int $cellSize,
    ) { }

    public function setCells(array $cells): void {
        $this->cells = $cells;
    }
   
    public function draw(): bool {
        $padding = ceil($this->cellSize * 0.6);
        $this->image = imagecreatetruecolor(
            width: $this->width*$this->cellSize+$padding*2,
            height: $this->height*$this->cellSize+$padding*2,
        );
        $this->lineWidth = $this->cellSize * 0.1;
        $bg = imagecolorallocate(
            $this->image, 0xff, 0xff, 0xff
        );
        $this->color1 = imagecolorallocate(
            $this->image, 0x33, 0x33, 0x33
        );
        $this->color2 = imagecolorallocate(
            $this->image, 0x99, 0x99, 0x99
        );
        imagefill($this->image, 0, 0, $bg);
        imagesetthickness($this->image, $this->lineWidth);
    
        $x = $y = $padding;
        foreach ($this->cells as $row) {
            foreach ($row as $cell) {
                $this->drawCell($cell, $x, $y);
                $x += $this->cellSize;
            }
            $x = $padding;
            $y += $this->cellSize;
        }
        return true;
    }
    //... other methods including drawCell() and save()
}

Recursive Backtracker Algorithm

The recursive backtracker algorithm works by picking a starting cell and then randomly breaking down walls to visit new cells. If the algorithm reaches a dead end, it backtracks to a previous cell and tries a different path. The code below shows a working example of the algorithm:

class RecursiveGenerator {
    private const WEST = 0x1;
    private const EAST = 0x2;
    private const SOUTH = 0x4;
    private const NORTH = 0x8;
    private const CLOSED = 15;
    private const BASH = [
        self::WEST => [
        'opposite' => self:: EAST,
        'xOffset' => -1,
        'yOffset' => 0
        ],
        self::EAST => [
        'opposite' => self:: WEST,
        'xOffset' => 1,
        'yOffset' => 0
        ],
        self::NORTH => [
        'opposite' => self:: SOUTH,
        'xOffset' => 0,
        'yOffset' => -1
        ],
        self::SOUTH => [
        'opposite' => self:: NORTH,
        'xOffset' => 0,
        'yOffset' => +1
        ],
    ];
    private array $cells;

    public function __construct(
    private int $maxRows,
    private int $maxCols
    ) {
        $this->cells = range(0, $maxRows - 1);
        $this->cells = array_map(
        function() use ($maxCols) {
        $row = range(0, $maxCols - 1);
        return array_map(fn() => self::CLOSED, $row);
        }, $this->cells);
    }
     public function getCells(): array
    {
        return $this->cells;
    }
    public function generate(): void {
        $startX = rand(0, $this->maxRows - 1);
        $startY = rand(0, $this->maxCols - 1);
        $this->visitCell($startX, $startY);
    }
    //... other methods including visitCell, getNeighborCoords, shuffleWalls
}

The code begins by initialising all the cells to have all four walls. The shuffleWalls() function returns a randomised array of available walls in the current cell. The visitCell() method recursively tries to break through all the walls.

Removing Dead Ends

To make the maze more solvable, we can remove dead ends. The function removeDeadEnds() scans the maze for cells with three walls and then breaks down one of the walls.

Adding Entrance and Exit

The addEntranceExit() function adds an entrance on the north side and an exit on the south side by removing the appropriate wall for a chosen cell. This ensures the maze is traversable.

Conclusion

Using the recursive backtracker algorithm with PHP, we can generate a variety of mazes. This approach can be used to generate mazes of different sizes and complexities.

       _
     _| |_
   _|     |_
  |   MAZE   |
   |_     _|
     |_|_|

Technology Index: Maze Generation, Recursive Backtracker, GD Library, Algorithms, Pseudocode, Data Structures.


First, Make it Easy: Refactoring for Rapid Change**

By Edward Barnard

Software projects can often become inflexible and hard to change, especially when deadlines are tight. This article explores a pragmatic refactoring technique to make codebases more adaptable to change without extensive rewrites.

The Challenge of Rigid Code

When PHP code becomes rigid and changes are needed urgently, you may need to apply a technique that allows for rapid refactoring. Often, it’s not possible to do a proper rewrite or add extensive tests. In these situations, the advice “For each desired change, make the change easy (warning: this may be hard), then make the easy change” becomes very useful.

The Problem: A 1300-Line Class

In a real-world scenario a 1300-line PHP class, StripePaymentController, needed updating. This class handled multiple types of registrations with complex and intertwined logic. This was hard to maintain because the application used different business workflows all in a single place. The difficulty arose due to an evolving database structure requiring corresponding changes, with different types of registration (regular season, league tournament, national championship and a special case tournament) all using the same class. This mixing of concerns made it hard to change the class safely.

Applying the Single Responsibility Principle

The Single Responsibility Principle suggests that a class should have only one reason to change. In this case, the different types of registration were all in the same class, violating this principle. The solution was to separate concerns and make changes easier.

The Solution: Extracting Methods into Classes

To make the necessary changes quickly and safely the solution is to extract methods into separate classes. To begin with all protected methods should be changed to private. This prevents the methods being called from outside the class. Next, each method of the original class was extracted into a new class of its own. For example, a method like captureFlow() was extracted into a new class CaptureFlow.

class CaptureFlow
{
    public function captureFlow(array $post): string
    {
        $flow = RegistrationWorkflow::captureFlow();
        $_SESSION['registration_flow'] = $flow;
        $_SESSION['stripe_post'] = $post;
        return $flow;
    }
}

Then the original class had to be changed to use the new class. The original method was deleted after all references to it had been replaced by the new class. This refactoring method continued for all of the original classes methods. The new initialize() method in StripePaymentController was used to instantiate all the new classes, including any dependencies.

The Result: Scattered Code

This method of refactoring scattered code from one large file into many small files. The original class was reduced

PHP Teacher magazine - May 2023
: PHP Teacher
https://phpteacher.com/blog/magazines/phpteacher-may-2023/
© 2025 PHP Teacher . All rights reserved.