Skip to content

Quick Reference: CRUD Operations in Slim MVC

This cheatsheet provides a quick reference for implementing CRUD operations in your Slim MVC application.

Checklist for Implementing CRUD Operations

Section titled “Checklist for Implementing CRUD Operations”

When implementing CRUD operations, you should follow these steps for each resource (e.g., products, categories, orders, users, etc.) in the following order:

  1. ✅ Create a Model class (extends BaseModel, inject PDOService)
  2. ✅ Create a Controller class (extends BaseController, inject Container and Model (s))
  3. ✅ Implement 7 standard controller methods (index, show, create, store, edit, update, delete)
  4. ✅ Create 4 view files (index, show, create, edit) for the resource
  5. ✅ Register 7 routes in web-routes.php for the resource
  6. ✅ Use named routes for redirects
  7. ✅ Follow RESTful URI patterns
  8. ✅ Validate input data before passing to model methods
  9. ✅ Use Post-Redirect-Get pattern (PRG) pattern after successful form submissions to prevent duplicate submissions
  10. ✅ Test all routes in browser

Extracting Input Data from the Client Request

Section titled “Extracting Input Data from the Client Request”

In your controller’s callback methods, you can extract the request data (route parameters, POST form data, query string parameters) from the request object using the following methods:

1. Get route parameter (from {id} placeholder)

Section titled “1. Get route parameter (from {id} placeholder)”
  • Inputs passed in the URI path are called route parameters.
  • Route parameters are typically used for parametric routes like /products/{id} and they help identify the specific item to be displayed, edited, or deleted. For example, /products/123
  • You can access them using the $args array parameter in your controller’s callback method.
app/Controllers/YourController.php
public function routeCallback(Request $request, Response $response, array $args): Response
{
$productId = (int) $args['id']; // E.g., /products/123 -> $args['id'] = '123'
}

  • Inputs passed in the POST request body are called form data.
  • Form data is typically used for form submissions or API requests. For example, /products
  • You can access them using the $request->getParsedBody() method.
app/Controllers/YourController.php
public function routeCallback(Request $request, Response $response, array $args): Response
{
$formData = $request->getParsedBody();
$productName = $formData['name'] ?? '';
$productPrice = $formData['price'] ?? 0;
}

  • Inputs passed in the GET request query string are called query string parameters.
  • Query string parameters are typically used for search queries or filters. For example, /products?search=laptop&sort=price
  • You can access them using the $request->getQueryParams() method.
app/Controllers/YourController.php
public function routeCallback(Request $request, Response $response, array $args): Response
{
$queryParams = $request->getQueryParams();
$search = $queryParams['search'] ?? '';
$sort = $queryParams['sort'] ?? 'name';
}

Create a controller class by extending BaseController. The constructor should call the parent constructor with the Container instance and inject model instance(s) into the controller’s constructor.

Standard Signatures of the Controller Callback Methods

Section titled “Standard Signatures of the Controller Callback Methods”
app/Controllers/YourController.php
<?php
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
class YourController extends BaseController
{
public function __construct(Container $container, private YourModel $model) {
parent::__construct($container);
}
// Display list of items (GET request)
public function index(Request $request, Response $response, array $args): Response
// Show single item details (GET request)
public function show(Request $request, Response $response, array $args): Response
// Display a form for creating a new item (GET request)
public function create(Request $request, Response $response, array $args): Response
// Handle a request to save a new item to the database (POST request)
public function store(Request $request, Response $response, array $args): Response
// Display a form for editing an item (GET request)
public function edit(Request $request, Response $response, array $args): Response
// Handle a request to save changes to an item (POST request)
public function update(Request $request, Response $response, array $args): Response
// Handle a request to delete an item (GET request)
public function delete(Request $request, Response $response, array $args): Response
}

Rendering Views and Redirecting to Other Routes

Section titled “Rendering Views and Redirecting to Other Routes”

The BaseController class provides two methods for displaying views and redirecting to other routes:

  1. render() → Display a view template using a view template file name (e.g., products/index.php)
  2. redirect() → Redirect to a named route using a named route name (e.g., products.index)

The render() method takes three parameters:

  1. $response → PSR-7 response object to modify
  2. $view_name → Name of the view template file (e.g., products/index.php)
  3. $data → Associative array of data to pass to the view (optional)
// Basic rendering
return $this->render($response, 'products/index.php');
// Passing associative array of data to view
$data = [
'product' => $this->model->findById((int) $args['id']),
'title' => 'Product Details'
];
return $this->render($response, 'products/show.php', $data);

  • The redirect() method is used to redirect the user to a named route.
  • It is typically used after a successful form submission (in the Post-Redirect-Get (PRG) pattern) to prevent duplicate submissions or after a data operation (like delete or update).

The redirect() method takes five parameters:

  1. $request → PSR-7 request object for route context
  2. $response → PSR-7 response object to modify
  3. $route_name → Named route to redirect to (e.g., products.index, products.edit)
  4. $uri_args → Replaces route placeholders like {id} in /products/{id} (optional)
  5. $query_params → Adds query strings like ?page=2&sort=name (optional)

Method Signature:

protected function redirect(
Request $request,
Response $response,
string $route_name, // Named route to redirect to
array $uri_args = [], // Optional: Replaces {placeholders} in route
array $query_params = [] // Optional: Adds ?key=value to URL
): Response

// Simple redirect (no parameters)
return $this->redirect($request, $response, 'home.index');
// With route parameter (replaces {id}) → /products/123
return $this->redirect($request, $response, 'products.show', ['id' => $productId]);
// With query parameters only → /products?category=electronics&sort=price
return $this->redirect($request, $response, 'products.index', [], [
'category' => 'electronics',
'sort' => 'price'
]);
// With BOTH route parameter and query string → /products/123/edit?tab=details
return $this->redirect($request, $response, 'products.edit', ['id' => $productId], [
'tab' => 'details'
]);

ActionMethodURI PatternController MethodRoute Name
List all itemsGET/productsindex()products.index
Show one itemGET/products/{id}show()products.show
Show create formGET/products/createcreate()products.create
Handle save new itemPOST/productsstore()-
Show edit formGET/products/{id}/editedit()products.edit
Save changesPOST/products/{id}update()-
Delete itemGET/products/{id}/deletedelete()products.delete

app/Routes/web-routes.php
use App\Controllers\ProductsController;
// 1) List all products (GET request)
$app->get('/products', [ProductsController::class, 'index'])
->setName('products.index'); // 👉 named route for redirecting after successful form submissions
// 2) Show single product (GET request)
$app->get('/products/{id}', [ProductsController::class, 'show'])
->setName('products.show');
// 3) Show a form for creating a new product (GET request)
$app->get('/products/create', [ProductsController::class, 'create'])
->setName('products.create');
// 4) Handle a request to save a new product to the database (POST request)
$app->post('/products', [ProductsController::class, 'store']);
// 5) Show a form for editing a product (GET request)
$app->get('/products/{id}/edit', [ProductsController::class, 'edit'])
->setName('products.edit');
// 6) Handle a request to save changes to a product (POST request)
$app->post('/products/{id}', [ProductsController::class, 'update']);
// 7) Handle a request to delete a product (GET request)
$app->get('/products/{id}/delete', [ProductsController::class, 'delete'])
->setName('products.delete');

The BaseModel class provides a set of convenient methods for database operations using prepared statements. All methods use named or positional parameters for security against SQL injection.

Create a model class by extending BaseModel. The constructor should call the parent constructor with the PDOService instance.

app/Models/YourModel.php
<?php
namespace App\Domain\Models;
use App\Helpers\Core\PDOService;
class ProductsModel extends BaseModel
{
public function __construct(PDOService $db_service) {
parent::__construct($db_service);
}
}

Retrieve Multiple Records using selectAll()

Section titled “Retrieve Multiple Records using selectAll()”
// Get all records
public function getAllProducts(): array
{
return $this->selectAll('SELECT * FROM products ORDER BY created_at DESC');
}
// With conditions (named parameters)
public function getProductsByCategory(int $categoryId): array
{
return $this->selectAll('SELECT * FROM products WHERE category_id = :id', ['id' => $categoryId]);
}

// Find by ID (positional parameter)
public function findById(int $id): array|false
{
return $this->selectOne('SELECT * FROM products WHERE id = ?', [$id]);
}
// Find by slug (named parameter)
public function findBySlug(string $slug): array|false
{
return $this->selectOne('SELECT * FROM products WHERE slug = :slug', ['slug' => $slug]);
}

// Count all
public function getTotalProducts(): int
{
return $this->count('SELECT COUNT(*) FROM products');
}
// Count with condition
public function countActiveProducts(): int
{
return $this->count('SELECT COUNT(*) FROM products WHERE status = :status', ['status' => 'active']);
}

// INSERT
public function create(array $data): int
{
return $this->execute(
'INSERT INTO products (name, price, created_at) VALUES (:name, :price, :created_at)',
['name' => $data['name'], 'price' => $data['price'], 'created_at' => date('Y-m-d H:i:s')]
);
}

// UPDATE
public function update(int $id, array $data): int
{
return $this->execute(
'UPDATE products SET name = :name, price = :price WHERE id = :id',
['id' => $id, 'name' => $data['name'], 'price' => $data['price']]
);
}

// DELETE
public function delete(int $id): int
{
return $this->execute('DELETE FROM products WHERE id = :id', ['id' => $id]);
}

public function createAndGetId(array $data): string
{
$this->execute('INSERT INTO products (name, price) VALUES (:name, :price)', $data);
return $this->lastInsertId();
}

  • Named parameters → Use :name for better readability in SQL queries
  • Route names → Always use ->setName() for routes you’ll redirect to (e.g., products.index, products.edit)
  • Route order → Place /create routes before /{id} routes
  • PRG pattern → Always redirect after POST requests to prevent duplicate submissions
  • Validation → Validate all input data before passing to model methods
  • Error handling → Use try-catch blocks and redirect on errors
  • Security → Use prepared statements (all BaseModel methods do this automatically)