Models¶
In LacePHP, a Model is your gateway to the database. It wraps a single table and provides simple methods for:
Creating, reading, updating and deleting rows (CRUD)
Querying with fluent where, orderBy, limit, etc.
Mass assignment of allowed fields
Automatic timestamps or refreshing generated columns
Defining relations (belongsTo, hasMany, hasOne, belongsToMany) and eager-loading
By using Models, you avoid repeating SQL in controllers and keep your data logic in one place.
Why Models Matter¶
DRY & Safe Put all your table logic in one class, and only allow writing of explicit columns via fillable.
Readable Code User::find(1) is easier to read and refactor than writing raw SQL everywhere.
Relations Load related rows (e.g. a post’s author or comments) with simple methods instead of manual joins.
Testability Swap in-memory arrays or mocks for Models in your tests, no need to hit a real database.
Defining Your First Model¶
Create a file weave/Models/Post.php:
<?php
namespace Weave\Models;
use Lacebox\Sole\Cobble\Model;
class Post extends Model
{
// Only these fields may be mass-assigned via constructor or save()
protected $fillable = ['title', 'body', 'user_id'];
}
This tells LacePHP that your table is posts (derived from Post) and only title, body and user_id can be set in bulk.
Basic CRUD Operations¶
In a controller you can now do:
use Weave\Models\Post;
class PostController
{
public function index(): string
{
// Fetch all posts
$posts = Post::all(); // returns array of Post objects
return kickback()->json($posts);
}
public function show($id): string
{
// Find by primary key (id)
$post = Post::find($id);
if (! $post) {
return kickback()->notFound("Post not found");
}
return kickback()->json($post);
}
public function store(): string
{
$data = sole_request()->only(['title','body','user_id']);
// Mass-assign and save
$post = new Post($data);
$post->save();
return kickback()->json($post, 201);
}
public function update(): string
{
$post = Post::find($id);
if (! $post) {
return kickback()->notFound("Post not found");
}
// Only fillable fields are updated
foreach (sole_request()->only(['title','body']) as $k => $v) {
$post->$k = $v;
}
$post->save();
return kickback()->json($post);
}
public function destroy($id): string
{
$post = Post::find($id);
if ($post) {
$post->delete();
}
return kickback()->text('Deleted', 204);
}
}
Query Builder & Fluent Queries¶
For more complex queries you can use the query() method:
// Get latest 5 posts by user 42
$recent = Post::query()
->where('user_id', '=', 42)
->orderBy('created_at', 'desc')
->limit(5)
->get();
// Count how many posts contain “LacePHP” in the title
$count = Post::query()
->where('title', 'LIKE', '%LacePHP%')
->count();
return kickback()->json(['recent' => $recent, 'count' => $count]);
Relations & Eager Loading¶
Define relations inside your Model by adding methods that call the helpers:
class Post extends Model
{
protected $fillable = ['title', 'body', 'user_id'];
// A Post belongs to one User
protected function author()
{
return $this->belongsTo(User::class, 'user_id');
}
// A Post has many Comments
protected function comments()
{
return $this->hasMany(Comment::class, 'post_id');
}
}
Then in your controller:
// Without eager loading (N+1 problem)
foreach (Post::all() as $post) {
$post->author; // fires one query per post
}
// With eager loading—loads authors in one query
$posts = Post::query()
->with(['author', 'comments'])
->get();
return kickback()->json($posts);
Note
Eager loading means fetching related data all at once instead of one item at a time. Without eager loading you might run into the “N+1 query” problem:
Without eager loading:
$posts = Post::all(); # 1 query for posts foreach ($posts as $post) { echo $post->author->name; # 1 extra query per post }
With eager loading:
$posts = Post::query() ->with(['author']) ->get(); # 1 query for posts + 1 for authors foreach ($posts as $post) { echo $post->author->name; # no extra queries }
Why it matters - Performance: far fewer database trips - Predictability: you know exactly how many queries will run - Readability: no hidden queries inside loops
Best Practices¶
Fillable Always set $fillable to avoid accidental mass-assignment of sensitive columns.
Custom table names If your table isn’t snake_case plural, set protected static $table = ‘my_table’;.
Single responsibility Keep business logic in services or separate classes—Models should focus on data access.
Cache heavy queries Combine with ShoeCacheKnots to cache expensive list or report queries.
By following these patterns, junior developers can harness LacePHP’s Model class to write clear, maintainable database code—no SQL required.