Skip to content

Commit bfdbc35

Browse files
authored
feat: implement phase 2 ORM improvements (#24)
* Implement phase 2 ORM improvements * Refine phase 2 query helpers * Fix cs fixer formatting
1 parent 51936a1 commit bfdbc35

21 files changed

+605
-56
lines changed

.php-cs-fixer.dist.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
<?php
22

3+
declare(strict_types=1);
4+
35
$finder = PhpCsFixer\Finder::create()
46
->in([
57
__DIR__ . '/src',

EXAMPLE.md

Lines changed: 52 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ echo $category->created_at;
3535
echo $category->updated_at;
3636
```
3737

38-
`save()` inserts when the primary key is empty and updates when the primary key is present.
38+
`save()` inserts when the primary key is `null` and updates when the primary key is present.
3939

4040
## Load a record by primary key
4141

@@ -93,17 +93,7 @@ $count = Category::count();
9393

9494
## Dynamic finders
9595

96-
Snake_case methods:
97-
98-
```php
99-
$all = Category::find_by_name('Fiction');
100-
$one = Category::findOne_by_name('Fiction');
101-
$first = Category::first_by_name(['Fiction', 'Fantasy']);
102-
$last = Category::last_by_name(['Fiction', 'Fantasy']);
103-
$count = Category::count_by_name('Fiction');
104-
```
105-
106-
CamelCase alternatives:
96+
Preferred camelCase methods:
10797

10898
```php
10999
$all = Category::findByName('Fiction');
@@ -113,6 +103,30 @@ $last = Category::lastByName(['Fiction', 'Fantasy']);
113103
$count = Category::countByName('Fiction');
114104
```
115105

106+
Legacy snake_case dynamic methods still work during the transition, but they are deprecated and should be migrated.
107+
108+
## Focused query helpers
109+
110+
Check existence:
111+
112+
```php
113+
$hasCategories = Category::exists();
114+
$hasFiction = Category::existsWhere('name = ?', ['Fiction']);
115+
```
116+
117+
Fetch ordered results:
118+
119+
```php
120+
$alphabetical = Category::fetchAllWhereOrderedBy('name', 'ASC');
121+
$latest = Category::fetchOneWhereOrderedBy('id', 'DESC');
122+
```
123+
124+
Pluck one column:
125+
126+
```php
127+
$names = Category::pluck('name', '', [], 'name', 'ASC', 10);
128+
```
129+
116130
## Custom WHERE clauses
117131

118132
Fetch a single matching record:
@@ -181,21 +195,43 @@ $rows = $statement->fetchAll(PDO::FETCH_ASSOC);
181195

182196
## Validation
183197

184-
Override `validate()` to enforce model rules before insert and update:
198+
Use instance-aware hooks to enforce model rules before insert and update:
185199

186200
```php
187201
class Category extends Freshsauce\Model\Model
188202
{
189203
protected static $_tableName = 'categories';
190204

191-
public static function validate()
205+
protected function validateForSave(): void
192206
{
193-
return true;
207+
if (trim((string) $this->name) === '') {
208+
throw new RuntimeException('Name is required');
209+
}
194210
}
195211
}
196212
```
197213

198-
Throw an exception from `validate()` whenever your application-specific rule fails, and the write will be aborted before insert or update.
214+
Override `validateForInsert()` or `validateForUpdate()` when the rules are operation-specific.
215+
216+
The legacy static `validate()` method remains supported for backward compatibility.
217+
218+
## Strict field mode
219+
220+
Enable strict field mode when you want unknown assignments to fail immediately instead of being silently ignored on persistence:
221+
222+
```php
223+
class Category extends Freshsauce\Model\Model
224+
{
225+
protected static $_tableName = 'categories';
226+
protected static bool $_strict_fields = true;
227+
}
228+
```
229+
230+
Or enable it temporarily:
231+
232+
```php
233+
Category::useStrictFields(true);
234+
```
199235

200236
## MySQL example connection
201237

README.md

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,16 @@ Model ORM for PHP
88

99
If you want database-backed PHP models without pulling in a heavyweight stack, this library is built for that job.
1010

11+
## When it fits
12+
13+
Use this library when you want:
14+
15+
- Active-record style models without adopting a full framework ORM
16+
- Direct access to SQL and PDO when convenience helpers stop helping
17+
- A small API surface that stays easy to understand in legacy or custom PHP apps
18+
19+
Skip it if you need relationship graphs, migrations, or a chainable query builder comparable to framework ORMs.
20+
1121
## Why teams pick it
1222

1323
- Lightweight by design: point a model at a table and start reading and writing records.
@@ -110,6 +120,22 @@ Category::countByName('Science Fiction');
110120

111121
Legacy snake_case dynamic methods still work during the transition, but they are deprecated and emit `E_USER_DEPRECATED` notices.
112122

123+
If you still have legacy calls such as `find_by_name()`, treat them as migration work rather than the preferred API.
124+
125+
### Focused query helpers
126+
127+
For common read patterns that do not justify raw SQL:
128+
129+
```php
130+
Category::exists();
131+
Category::existsWhere('name = ?', ['Science Fiction']);
132+
133+
$ordered = Category::fetchAllWhereOrderedBy('name', 'ASC');
134+
$latest = Category::fetchOneWhereOrderedBy('id', 'DESC');
135+
136+
$names = Category::pluck('name', '', [], 'name', 'ASC', 10);
137+
```
138+
113139
### Flexible SQL when convenience methods stop helping
114140

115141
Use targeted where clauses:
@@ -133,21 +159,39 @@ $rows = $statement->fetchAll(PDO::FETCH_ASSOC);
133159

134160
### Validation hooks
135161

136-
Override `validate()` in your model when writes need application rules:
162+
Use instance-aware hooks when writes need application rules:
137163

138164
```php
139165
class Category extends Freshsauce\Model\Model
140166
{
141167
protected static $_tableName = 'categories';
142168

143-
public static function validate()
169+
protected function validateForSave(): void
144170
{
145-
return true;
171+
if (trim((string) $this->name) === '') {
172+
throw new RuntimeException('Name is required');
173+
}
146174
}
147175
}
148176
```
149177

150-
Throw an exception from `validate()` to block invalid inserts or updates.
178+
Use `validateForInsert()` or `validateForUpdate()` when the rules differ by operation.
179+
180+
The legacy static `validate()` method still works for backward compatibility, but new code should prefer the instance hooks.
181+
182+
### Strict field mode
183+
184+
Unknown properties are permissive by default for compatibility. If you want typo-safe writes, enable strict field mode:
185+
186+
```php
187+
class Category extends Freshsauce\Model\Model
188+
{
189+
protected static $_tableName = 'categories';
190+
protected static bool $_strict_fields = true;
191+
}
192+
```
193+
194+
You can also opt in at runtime with `Category::useStrictFields(true)`.
151195

152196
### Predictable exceptions
153197

src/Model/Exception/ConfigurationException.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
<?php
22

3+
declare(strict_types=1);
4+
35
namespace Freshsauce\Model\Exception;
46

57
class ConfigurationException extends ModelException

src/Model/Exception/ConnectionException.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
<?php
22

3+
declare(strict_types=1);
4+
35
namespace Freshsauce\Model\Exception;
46

57
class ConnectionException extends ModelException

src/Model/Exception/InvalidDynamicMethodException.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
<?php
22

3+
declare(strict_types=1);
4+
35
namespace Freshsauce\Model\Exception;
46

57
class InvalidDynamicMethodException extends ModelException

src/Model/Exception/MissingDataException.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
<?php
22

3+
declare(strict_types=1);
4+
35
namespace Freshsauce\Model\Exception;
46

57
class MissingDataException extends ModelException

src/Model/Exception/ModelException.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
<?php
22

3+
declare(strict_types=1);
4+
35
namespace Freshsauce\Model\Exception;
46

57
class ModelException extends \RuntimeException

src/Model/Exception/UnknownFieldException.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
<?php
22

3+
declare(strict_types=1);
4+
35
namespace Freshsauce\Model\Exception;
46

57
class UnknownFieldException extends ModelException

0 commit comments

Comments
 (0)