Members | Series | Title | Release | Length |
---|---|---|---|---|
Pragmatic PHP |
A Simple Router
Time to give our app some direction—literally! In this video, we introduce a simple but powerful router to handle page requests with clarity and control. 🧭
Here’s what we’ll build: • Refactor pages into controller classes: • app/Http/AboutController.php • app/Http/UploadController.php • app/Http/PhotoController.php 📂 • Set up Composer PSR-4 autoloading for the App\\ namespace ⚙️ • Build routing logic in index.php, then move it to app/routes.php and eventually into globals.php for reuse 🔁 By the end, our app will be running on a clean routing system—flexible, readable, and easy to extend. Welcome to version v6-create-simple-router. This is where things start to feel real. 🚀 |
Tue, Apr 29, 2025 | 14m:47s | |
Pragmatic PHP |
Handling "Bad" Routes
Not all roads lead somewhere… and that’s okay—as long as we handle it gracefully! In this video, we improve the robustness of our app by dealing with invalid routes and unexpected errors the pragmatic way. 🛡️
Here’s what we’ll do: • Make the header element optional in layout/_header.view.php using isset() 🧩 • Create clean, user-friendly error views: • views/errors/404.view.php 🔍 • views/errors/500.view.php 💥 • Add a global try-catch block in index.php to handle exceptions gracefully 🧯 With this in place, our app won’t crash or confuse users when something goes wrong. Instead, it responds with clarity and professionalism. By the end of this video, we’ll be on version v7-handle-bad-routes—stable, polished, and production-ready. 🚀 |
Thu, May 1, 2025 | 6m:26s | |
Pragmatic PHP |
Creating A Master Layout
Let’s streamline our app’s structure even further! In this video, we tackle layout repetition by introducing a master layout—making our pages cleaner and more consistent. 🧼
Here’s what we’ll cover: • Refactor repetitive layout code into layout/app.view.php using a $page_content placeholder for dynamic content 🧩 • Enhance our view() helper function to accept a third $layout parameter 🧠 • Update controllers to support a page_layout variable for flexibility in rendering 🛠️ • Clean up our individual page views for clarity and focus ✂️ • Bonus: Improve our dd() helper to accept any number of arguments using ...$var 🧪 By the end of this refactor, we’ll be rocking version v8-create-layout—a polished, DRY, and maintainable foundation for future growth. 🚀 |
Tue, May 6, 2025 | 8m:23s | |
Pragmatic PHP |
View Infrastructure
It’s time to level up our view system! In this video, we refactor our simple view() helper into a flexible View class, giving us cleaner, more expressive control over how pages are rendered. ✨
Here’s what we’ll do: • Create the app/Framework directory to hold core infrastructure 🗂️ • Build the View class to support a fluent, chainable syntax for setting views, layouts, data, and page titles 🔧 • Add support for setting a custom page title directly in the view pipeline 🏷️ • Update controller classes to return View instances instead of using the old helper 🔄 • Refactor the load_route function and index.php to support the new view rendering flow 🔁 By the end, we’ll arrive at version v9-view-infrastructure—a modern, powerful view layer that keeps your code clean, expressive, and easy to maintain. 🚀 |
Thu, May 8, 2025 | 12m:15s | |
Pragmatic PHP |
Router Infrastructure
Let’s bring structure and scalability to our routing system! In this video, we encapsulate all routing logic into a dedicated Router infrastructure class, giving us a clean and centralized way to manage routes. 🧭
Here’s what we’ll accomplish: • Move and rename load_route() into the new Router class as a static view() method 🔁 • Add static get() and post() methods to define routes declaratively 🚪 • Simplify how our application connects URLs to controllers using a centralized approach 🧠 By the end, we’ll have version v10-router-infra—a solid foundation for routing that’s clean, powerful, and easy to scale. 🚀 |
Tue, May 13, 2025 | 10m:34s | |
Pragmatic PHP |
Database Connections / Migrations
Let’s connect our app to the real world of data! In this video, we build a reliable and secure database layer and run our first migrations and seed scripts. This step sets the stage for persistent data handling in our application. 🧱
Here’s what we cover: • Temporarily hijack index.php for local development and testing purposes 🔧 • Add database/migrate.sql and database/seed.sql to define and populate our schema 🧬 • Use file_get_contents() to load SQL scripts and run them using PDO 🎯 • Define connection settings: host, port, database name, user, and password (with Docker-friendly defaults) 🐳 • Set up secure PDO options for error handling and safe queries 🔐 • Wrap everything in a try/catch to safely execute migrations and seeding 🚨 • Introduce our app’s database structure: • users – for login info 👤 • photos – uploaded images 📸 • reviews – ratings and comments ⭐📝 • Protect sensitive credentials by moving them into a .env file 🛡️ • Create a Framework/Env.php class to access environment variables cleanly 🌱 By the end of this video, version v11-db-basics will bring real data into play—securely, cleanly, and pragmatically. 🚀 |
Thu, May 15, 2025 | 21m:3s | |
Pragmatic PHP |
Database Infrastructure
Let’s encapsulate our database logic the pragmatic way! In this video, we refactor our raw database code into a dedicated Database class, making connections and queries more reusable, readable, and testable. 🛠️
Here’s what we’ll do: Create a Framework/Database class that accepts config parameters in the constructor ⚙️ Expose an exec() method for executing raw SQL 🧾 Add a static instance for quick access to a shared connection 🌐 Introduce helper methods: first() to fetch a single record & all() to fetch multiple records Write a simple query to fetch a photo by ID and pass it into the view dynamically 📸 Update the View class to properly render passed variables inside templates Introduce a private render_template() method to handle this cleanly 🧩 By the end of this refactor, we’ll be on version v12-db-encapsulation—with database access that’s clean, powerful, and built to scale. 🚀 |
Tue, May 20, 2025 | 20m:53s | |
Pragmatic PHP |
Dynamic Photo Page
Let’s make our photo pages come alive! In this video, we turn the static photo view into a fully dynamic experience by using the $_GET superglobal to load data based on query parameters. 🔍
Here’s what we tackle: Update the PhotoController to read the id from $_GET and query the database accordingly Show a 404 error if the id is missing or the record isn’t found 🛑 Improve our error views (404 and 500) by rendering them within the layout for a consistent look 🎨 Refactor the View class to default to the main layout (layout.app) while still allowing overrides Fix a potential infinite render loop in the layout logic 🐛 Clean up unnecessary calls to ->layout() by relying on the new default behavior 🧼 By the end, version v13-photo-query-param will serve up the right photo with the right layout—dynamically and elegantly. 🚀 |
Thu, May 22, 2025 | 7m:43s | |
Pragmatic PHP |
Dynamic Photo Gallery
Time to showcase our images! In this video, we build a photo gallery page that lists all uploaded photos and links each one to its detailed view. 🧱
Here’s what we’ll do: Create a new show.view.php by copying and modifying index.view.php 🧬 Add a new route and controller method to handle the gallery page 🗺️ Display all photos in a clean list or grid, each with a clickable link to its detailed photo page 🔗 With version v14-photos-list, our app now lets users browse through all uploaded images—laying the foundation for interactivity and exploration. 🚀 |
Tue, May 27, 2025 | 4m:26s | |
Pragmatic PHP |
Displaying Reviews
Let’s bring feedback into the spotlight! In this video, we enhance the photo detail page by fetching and displaying user reviews from the database. 💬
Here’s what we’ll cover: Query the database for all reviews associated with a given photo 📄 Loop through the reviews and display them on the photo page 🎞️ Calculate a summary (like average stars or total reviews) 📊 Display the summary alongside the photo for quick insight 🧠 By the end, version v15-show-reviews adds real voices to each photo—making the app feel more alive and interactive. 🚀 |
Thu, May 29, 2025 | 19m:34s | |
Pragmatic PHP |
First Review & Refactor
It’s time to pause and sharpen the tools. In this video, we take a step back to review our progress and clean up our codebase, because maintainability is key to sustainable development. 🛠️
Here’s what we tackle in version v16-framework-refactor: Emphasize the value of regular review and refactor sessions 🧠 Review our entry point (public/index.php) and extract logic into a new App class 📦 Create Framework/App as a singleton with a start() method to centralize bootstrapping 🚀 Address messy relative paths like '../../' by creating a dedicated Path helper class 🧭 Build Framework/Path as a singleton and introduce helpers like app(), root(), require_app(), and require_root() Refactor all path-related code to use these clean, expressive helpers 📁 Move globals.php into the Framework directory to keep framework-related code encapsulated Clean up and clarify all require/import statements 🔄 This refactor sets us up for a cleaner, more organized foundation—making our mini-framework easier to navigate, extend, and love. 💡 |
Tue, Jun 3, 2025 | 18m:18s | |
Pragmatic PHP |
Submitting Reviews
It’s time to let users have their say! In this video, we implement the “Leave a Review” feature, allowing users to submit feedback on photos directly from the app. 💬
Here’s what’s packed into version v17-leave-review: Add a POST route to handle form submissions 🛤️ Stub out the PhotoController::store method as our entry point for review handling Wire up the review form and hook it into the controller 🧵 Introduce basic validation to catch missing fields or bad data 🧼 Add temporary dd('TODO') calls as placeholders for future redirects Build the SQL insert logic using the photo_id from the query string 🔗 Validate that the photo exists before inserting a review to maintain data integrity 🔒 Refactor the Database class: Rename exec() to raw() for clarity Add a new execute() method to handle parameterized inserts and return the number of affected rows ⚙️ By the end of this video, users can leave their thoughts—and our app takes one more step toward being fully interactive. 🌟 |
Thu, Jun 5, 2025 | 29m:47s | |
Pragmatic PHP |
Sessions / Redirects / Flash Messages
Now that users can submit reviews, let’s give them feedback! In this video, we bring in sessions, redirects, and flash messaging to guide the user experience after form submissions. ✨
Here’s what gets built in version v18-redirects-with-flash: - Create a Session class as a singleton, with a handy global session() helper 🔐 - Initialize the session at app start and build a clean API like: - session()->error('Missing required parameters.')->redirect('/'); - Implement error() and redirect() methods for smooth flow and messaging - Fix the issue of persistent flash messages by introducing delete_transient_data() to clear session data after one request cycle - Use Alpine.js to display flash messages dynamically with a snappy user experience ⚡ - Create a reusable _flash.view.php layout file for flash UI 💡 - Hook flash messages into the review flow by updating all dd('TODO') redirects in the PhotoController - Bonus: Add current_route support in the Router to improve context-awareness in views 🧭 With sessions and flash messages in place, your app now speaks back to the user—clear, friendly, and fast. 🚀 |
Tue, Jun 10, 2025 | 25m:6s | |
Pragmatic PHP |
Invalid Form Submissions
In this video, we tackle the next critical part of the form experience—handling invalid submissions with grace. When a user submits a form with missing or incorrect data, we want to redirect them back, repopulate the form, and show helpful error messages. 💡
Here’s what we build in version v19-redirect-with-errors: - Define the ideal usage: - session()->invalid($errors)->redirect_back(); - Then… we make it real! 💥 - Introduce invalid() and redirect_back() methods in the Session class - Store transient $old and $errors values to survive the redirect and improve UX ✨ - Refactor Session::old() into a more flexible Session::flash() and flash_message() pattern 🔁 - Improve the old($key, $default) helper to escape HTML using htmlspecialchars() for security 🛡️ - Add a validation_message() helper to display specific errors next to form fields - Update the review form: - Loosen validation so comment is optional - Add hidden rating input with default of 0 - Test with extreme values (e.g., rating=10, long name) to confirm validation is triggered - Clean up temporary debug outputs (like var_dump()) and finalize the flow ✅ This update makes your app smarter, friendlier, and much more user-friendly when things go wrong. A polished user experience, even on failure? Now that’s pragmatic. 🚀 |
Thu, Jun 12, 2025 | 18m:5s | |
Pragmatic PHP |
Database Model Abstractions
In this video, we take a big step toward cleaner, more maintainable code by introducing a Model abstraction layer—a pattern that reduces boilerplate and centralizes logic around database operations. This lands in version v20-db-models.
Here’s what gets done: ✅ Create a Models/Photo class - Move the all() and find() logic out of PhotoController and into this model ✅ Create a Models/Review class - Same deal: centralize data logic away from controllers - Introduce a protected static ?string $table property 🧱 Extract shared logic into a Framework/Model base class: - static table() throws by default—models must define their own table - Implement reusable all() and find() methods 🔄 Refactor all() and find() with: - static::query() → creates an instance of the model - get() → used by all(), calls build_query() to prepare the SQL and params - build_query() → generates the SQL from query state 🧠 Reimagine find() usage like this: - static::query()->where('id', '=', $id)->first(); - Then implement: - A where() method that collects conditions into $wheres - Modify build_query() to handle WHERE clauses - A first() method to return the first matching result 🚀 Refactor the PhotoController to use the new model APIs - Add an insert() method on the base Model class for creating new records By the end of this refactor, your models are smart, reusable, and expressive—freeing your controllers from repetitive SQL and focusing them on application logic. Pragmatic PHP is feeling pretty elegant right about now. 👌 |
Tue, Jun 17, 2025 | 26m:22s | |
Pragmatic PHP |
Fluent Validation
In this step, we introduced a flexible and expressive validation system.
🧱 Created the Framework/Validation class and added a validate() helper to globals.php. ✍️ The goal was to write intuitive validation logic like: validate('rating')->integer()->min(0)->max(5)->required(); validate('name')->string()->max(100); validate('comment')->string()->max(1000); 🧪 This fluent API allowed chaining validation rules for each input field, improving both readability and maintainability. 🔧 We fixed the num_stars constraint in the migration SQL to properly enforce the rating range. 🧹 Ran the migration and reseeded the database to apply the changes. 🕵️ Also refined the reviews logic to default the name to “Anonymous” when one isn’t provided. This new validation infrastructure enhances data safety and gives us a consistent, clean way to validate user input throughout the application. |
Thu, Jun 19, 2025 | 17m:11s | |
Pragmatic PHP |
User Registration
🔹 Controller Setup
- Create a new RegistrationController inside Http/, with index() and store() methods - index() displays the registration form - store() temporarily calls dd($_POST) to confirm input 🔹 View - Add the form view at views/registration.index.view.php 🔹 Validation - Write validation rules in fluent style: - $email = validate('email')->email()->required()->unique('users', 'email'); - Build out the validation logic to support this pattern 🔹 User Model - Create a Models/User class - Move relevant DB logic from the controller into the model 🔹 Model Base Enhancements - In the Model base class: - Add $is_count to build_query() - Implement a count() method that supports basic tallying - In the Database class: - Add a count() method to execute counting queries - Add a unique() validation method using model and count 🔹 Data Insertion - Assemble a $data array with validated inputs - Call User::insert($data) to create the new user That’ll wrap up the registration feature and land us at v22-registering-users 🏁 |
Tue, Jun 24, 2025 | 20m:42s | |
Pragmatic PHP |
User Authentication
📁 Controller Setup
- Create a new AuthenticationController inside Http/ with three methods: - login() – shows the login form - authenticate() – handles login logic - logout() – clears user session 📄 View - Add views/login/index.view.php to render the login form 🔀 Routing - Register the necessary routes: - GET /login → shows the login form - POST /login → processes authentication - POST /logout (or GET) → logs the user out 🧠 Authentication Logic - Inside authenticate(): - Validate email and password inputs - Lookup user by email from the database - Use password_verify() to check if password matches - On success → Session::login($user_id) - On failure → redirect back with error message 🔐 Session Enhancements - Add login($user_id) to store user ID in session - Add logout() to destroy the session cleanly 📌 And that lands us at v23-authentication. |
Thu, Jun 26, 2025 | 9m:3s | |
Pragmatic PHP |
Auth & Application Workflow
🛠️ Session Enhancements
- Add guest() and user() methods to the Session class - guest() → returns true if no user is logged in - user() → returns current user object (or null) 🎨 Navigation Improvements - Update the top navigation bar to reflect authentication state: - Show Register / Login links when user is not logged in - Show user avatar and menu when logged in - Fix any related JavaScript issues 👤 Gravatar Integration - Create a new helper class: app/Code/Gravatar.php - Use the user’s email to generate their avatar - Display avatar in the nav for logged-in users 🖼️ Conditional Upload View - Modify the upload page to only display the upload form if the user is logged in 📌 And that ties up the loose ends, landing us at v24-authentication-loose-ends. |
Tue, Jul 1, 2025 | 15m:30s | |
Pragmatic PHP |
Uploading Photos
🔒 UI Adjustments Based on Auth State
• Hide the notification bell in the top nav if the user isn’t logged in 🧭 Routing & Form Setup • Create an /upload route • Set the form’s action and stub in the store() method with a dd($_POST) to start • Remove the unused “title” field from the upload form 🧪 Validation Process • Define validation for the uploaded photo: $file = validate('photo')->file($allowed_mimes)->max(4016053)->required(); • Add $allowed_mimes and $max_file_size constants to the Photo model • Implement file() and update the max() method in the Validation class • Fix Validation::clean_value() to properly handle file inputs ⚠️ Missing Validation Messages • Ensure validation errors are displayed in these views: • registration.index • auth.index • upload.index 💾 File Storage & DB Entry • Save the uploaded file to the appropriate location • Create a new photo record in the database 🚫 .gitignore Update • Add public/photos to the .gitignore file to prevent uploaded files from being committed ⸻ 📌 And that completes the upload feature, bringing us to v25-upload-photos. Ready to show off some pictures! |
Thu, Jul 3, 2025 | 19m:17s | |
Pragmatic PHP |
Photo Deletion
🖼️ Improved Photo Show Page
In photo.show, hide the summary and reviews if none exist 🔗 Delete Route & Form Add a new route using the DELETE method In photo.show, display a delete form only if the logged-in user owns the photo Include a hidden input field: _METHOD = DELETE Update the Router class to detect the method via: $_POST['_METHOD'] ?? $_SERVER['REQUEST_METHOD'] 🔧 Controller Refactor Split responsibilities: ReviewController: keep only the store method, and fix redirect_back() usage PhotoController: remove the store method 🧨 Destroy Method in PhotoController Ensure the user is authenticated Validate the photo exists Validate ownership of the photo Delete the photo file: Add directory() method to the Photo model Add convert_to_path() method to the Photo model Delete the corresponding database record: Add a delete() method to the base Model class 📌 That wraps up the deletion flow and lands us at v26-delete-photos — now users can clean up after themselves! |
Tue, Jul 8, 2025 | 22m:11s | |
Pragmatic PHP |
Second Refactor & Review
🧠 Model Hydration Fixes
- Solved the issue where results weren’t instances of the child model class - Introduced hydrate() and hydrate_rows() to handle model instantiation - Added PHPDoc annotations with generics to assist with static analysis and better typing - Updated type hints across Policies and Controllers for clarity and consistency 📦 Model Enhancements - Added an update() method - Introduced pagination support with skip() and take() - Added sorting via order_by() and order_by_desc() - Modified / route to use pagination 🛡️ New Policy Classes - PhotoController: created Http/Policies/PhotoPolicy - AuthController: created and used AuthPolicy - RegisterController: created UserPolicy - ReviewController: created ReviewPolicy - UploadController: updated to use PhotoPolicy - Fixed return $data in UploadController to include a proper URL 📌 All of that polish and organization lands us at v26-review-refactor — making the codebase smoother, smarter, and more maintainable. |
Thu, Jul 10, 2025 | 19m:31s | |
Pragmatic PHP |
Blade / Twig Style Templates
In this video, we upgrade our templating engine to support Blade-style syntax, bringing a more expressive and readable feel to our views. Here’s what we did:
Enhanced the View class to support: - Escaped output: {{ $user->name }} - Raw output: {!! $user->name !!} Updated all relevant views to use the new shorthand syntax for cleaner templates. - 🛠️ Fixed a display issue in photo.show where old_rating wasn’t mapping correctly (it was showing a 4 instead of the correct 1). ✨ With this little dose of syntax sugar, we land at v27-blade-envy — giving our views a modern touch without bringing in a full framework. |
Tue, Jul 15, 2025 | 7m:48s | |
Pragmatic PHP |
Final Review & Refactor
This video is all about the last layer of polish — a final walkthrough to tidy up code, views, and structure across the project. Here’s what we touched:
🖼️ Views - index.view.php: added wider screen support with max-w-7xl. - Removed a duplicate header to clean up the layout. 🧱 Framework Cleanup - Database.php: fixed IDE issues and added helpful annotations. - globals.php: minor tweaks to smooth dev experience. - Model.php: highlighted PHPDoc comments, reviewed skip(), take(), and orderBy() methods. - Session.php and View.php: both got the “FII” treatment (Fix IDE Issues). 📦 Http Layer - Policies: - UserPolicy: FII. - Controllers: - AuthenticationController, PhotoController, RegisterController, ReviewController, and UploadController: all reviewed and cleaned up. 🌐 Public Entry Point - index.php: adjusted try/catch block for better error handling and clarity. 🎯 And that wraps things up at v28-review-refactor — setting the stage for a maintainable, scalable app with a solid foundation. |
Thu, Jul 17, 2025 | 12m:54s |