Skip to content

Commit 6ccece4

Browse files
authored
refactor: change magic model methods to camelCase (#19)
* Change magic model methods to camelCase * Update README for camelCase dynamic methods * Address PR review comments
1 parent 2a476f9 commit 6ccece4

4 files changed

Lines changed: 234 additions & 87 deletions

File tree

README.md

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ It is designed for projects that value straightforward PHP, direct PDO access, a
1313
- Minimal setup: define a model class and table name, then start reading and writing rows.
1414
- PDO-first: use the ORM helpers when they help and drop down to raw SQL when they do not.
1515
- Familiar model flow: create, hydrate, validate, save, update, count, find, and delete.
16-
- Dynamic finders: call methods such as `find_by_name()`, `findOneByName()`, `count_by_name()`, and more.
16+
- Dynamic finders: call methods such as `findByName()`, `findOneByName()`, `countByName()`, and more.
1717
- Multi-database support: tested against MySQL/MariaDB and PostgreSQL, with SQLite code paths also supported.
1818

1919
## Installation
@@ -118,16 +118,18 @@ Timestamp columns named `created_at` and `updated_at` are populated automaticall
118118

119119
### Dynamic finders and counters
120120

121-
You can query using snake_case or CamelCase method names:
121+
You can query using camelCase dynamic method names:
122122

123123
```php
124-
Category::find_by_name('Science Fiction');
125-
Category::findOne_by_name('Science Fiction');
126-
Category::first_by_name(['Sci-Fi', 'Fantasy']);
124+
Category::findByName('Science Fiction');
125+
Category::findOneByName('Science Fiction');
126+
Category::firstByName(['Sci-Fi', 'Fantasy']);
127127
Category::lastByName(['Sci-Fi', 'Fantasy']);
128-
Category::count_by_name('Science Fiction');
128+
Category::countByName('Science Fiction');
129129
```
130130

131+
Legacy snake_case dynamic methods remain available during the transition, but they are deprecated and emit `E_USER_DEPRECATED` notices.
132+
131133
### Custom where clauses
132134

133135
When you need more control, fetch one or many records with SQL fragments:

src/Model/Model.php

Lines changed: 151 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -490,16 +490,16 @@ public static function last(): ?static
490490
*/
491491
public static function find($id)
492492
{
493-
$find_by_method = 'find_by_' . (static::$_primary_column_name);
494-
return static::$find_by_method($id);
493+
return static::fetchAllWhereMatchingSingleField(static::resolveFieldName(static::$_primary_column_name), $id);
495494
}
496495

497496
/**
498-
* handles calls to non-existant static methods, used to implement dynamic finder and counters ie.
499-
* find_by_name('tom')
500-
* find_by_title('a great book')
501-
* count_by_name('tom')
502-
* count_by_title('a great book')
497+
* handles calls to non-existent static methods, used to implement dynamic finder and counters ie.
498+
* findByName('tom')
499+
* findByTitle('a great book')
500+
* countByName('tom')
501+
* countByTitle('a great book')
502+
* snake_case dynamic methods remain temporarily supported and trigger a deprecation warning.
503503
*
504504
* @param string $name
505505
* @param array $arguments
@@ -509,52 +509,135 @@ public static function find($id)
509509
*/
510510
public static function __callStatic($name, $arguments)
511511
{
512-
// Note: value of $name is case sensitive.
513512
$match = $arguments[0] ?? null;
514-
if (preg_match('/^find_by_/', $name) == 1) {
515-
// it's a find_by_{fieldname} dynamic method
516-
$fieldname = substr($name, 8); // remove find by
517-
return static::fetchAllWhereMatchingSingleField(static::resolveFieldName($fieldname), $match);
518-
} elseif (preg_match('/^findOne_by_/', $name) == 1) {
519-
// it's a findOne_by_{fieldname} dynamic method
520-
$fieldname = substr($name, 11); // remove findOne_by_
521-
return static::fetchOneWhereMatchingSingleField(static::resolveFieldName($fieldname), $match, 'ASC');
522-
} elseif (preg_match('/^first_by_/', $name) == 1) {
523-
// it's a first_by_{fieldname} dynamic method
524-
$fieldname = substr($name, 9); // remove first_by_
525-
return static::fetchOneWhereMatchingSingleField(static::resolveFieldName($fieldname), $match, 'ASC');
526-
} elseif (preg_match('/^last_by_/', $name) == 1) {
527-
// it's a last_by_{fieldname} dynamic method
528-
$fieldname = substr($name, 8); // remove last_by_
529-
return static::fetchOneWhereMatchingSingleField(static::resolveFieldName($fieldname), $match, 'DESC');
530-
} elseif (preg_match('/^count_by_/', $name) == 1) {
531-
// it's a count_by_{fieldname} dynamic method
532-
$fieldname = substr($name, 9); // remove find by
533-
return static::countByField(static::resolveFieldName($fieldname), $match);
534-
} elseif (preg_match('/^findBy/', $name) == 1) {
535-
// it's a findBy{Fieldname} dynamic method
536-
$fieldname = substr($name, 6); // remove findBy
537-
return static::fetchAllWhereMatchingSingleField(static::resolveFieldName($fieldname), $match);
538-
} elseif (preg_match('/^findOneBy/', $name) == 1) {
539-
// it's a findOneBy{Fieldname} dynamic method
540-
$fieldname = substr($name, 9); // remove findOneBy
541-
return static::fetchOneWhereMatchingSingleField(static::resolveFieldName($fieldname), $match, 'ASC');
542-
} elseif (preg_match('/^firstBy/', $name) == 1) {
543-
// it's a firstBy{Fieldname} dynamic method
544-
$fieldname = substr($name, 7); // remove firstBy
545-
return static::fetchOneWhereMatchingSingleField(static::resolveFieldName($fieldname), $match, 'ASC');
546-
} elseif (preg_match('/^lastBy/', $name) == 1) {
547-
// it's a lastBy{Fieldname} dynamic method
548-
$fieldname = substr($name, 6); // remove lastBy
549-
return static::fetchOneWhereMatchingSingleField(static::resolveFieldName($fieldname), $match, 'DESC');
550-
} elseif (preg_match('/^countBy/', $name) == 1) {
551-
// it's a countBy{Fieldname} dynamic method
552-
$fieldname = substr($name, 7); // remove countBy
553-
return static::countByField(static::resolveFieldName($fieldname), $match);
513+
$dynamicMethod = static::parseDynamicStaticMethod($name);
514+
if (is_array($dynamicMethod)) {
515+
if ($dynamicMethod['deprecated']) {
516+
static::triggerSnakeCaseDynamicMethodDeprecation($name);
517+
}
518+
return static::dispatchDynamicStaticMethod($dynamicMethod['operation'], $dynamicMethod['fieldname'], $match);
554519
}
555520
throw new \Exception(__CLASS__ . ' not such static method[' . $name . ']');
556521
}
557522

523+
/**
524+
* Parse supported dynamic static finder/counter names.
525+
*
526+
* @param string $name
527+
*
528+
* @return array{operation: string, fieldname: string, deprecated: bool}|null
529+
*/
530+
protected static function parseDynamicStaticMethod(string $name): ?array
531+
{
532+
$camelCasePrefixes = array(
533+
'findOneBy' => 'findOne',
534+
'findBy' => 'findAll',
535+
'firstBy' => 'first',
536+
'lastBy' => 'last',
537+
'countBy' => 'count',
538+
);
539+
foreach ($camelCasePrefixes as $prefix => $operation) {
540+
if (str_starts_with($name, $prefix)) {
541+
$fieldname = substr($name, strlen($prefix));
542+
if ($fieldname === '') {
543+
return null;
544+
}
545+
return array(
546+
'operation' => $operation,
547+
'fieldname' => $fieldname,
548+
'deprecated' => false,
549+
);
550+
}
551+
}
552+
553+
$snakeCasePrefixes = array(
554+
'findOne_by_' => 'findOne',
555+
'find_by_' => 'findAll',
556+
'first_by_' => 'first',
557+
'last_by_' => 'last',
558+
'count_by_' => 'count',
559+
);
560+
foreach ($snakeCasePrefixes as $prefix => $operation) {
561+
if (str_starts_with($name, $prefix)) {
562+
$fieldname = substr($name, strlen($prefix));
563+
if ($fieldname === '') {
564+
return null;
565+
}
566+
return array(
567+
'operation' => $operation,
568+
'fieldname' => $fieldname,
569+
'deprecated' => true,
570+
);
571+
}
572+
}
573+
574+
return null;
575+
}
576+
577+
/**
578+
* Execute a parsed dynamic static method.
579+
*
580+
* @param string $operation
581+
* @param string $fieldname
582+
* @param mixed $match
583+
*
584+
* @return mixed
585+
* @throws \Exception
586+
*/
587+
protected static function dispatchDynamicStaticMethod(string $operation, string $fieldname, $match)
588+
{
589+
$resolvedFieldname = static::resolveFieldName($fieldname);
590+
591+
return match ($operation) {
592+
'findAll' => static::fetchAllWhereMatchingSingleField($resolvedFieldname, $match),
593+
'findOne' => static::fetchOneWhereMatchingSingleField($resolvedFieldname, $match, 'ASC'),
594+
'first' => static::fetchOneWhereMatchingSingleField($resolvedFieldname, $match, 'ASC'),
595+
'last' => static::fetchOneWhereMatchingSingleField($resolvedFieldname, $match, 'DESC'),
596+
'count' => static::countByField($resolvedFieldname, $match),
597+
default => throw new \Exception(static::class . ' not such static method operation[' . $operation . ']'),
598+
};
599+
}
600+
601+
/**
602+
* Warn when a deprecated snake_case dynamic method is used.
603+
*
604+
* @param string $name
605+
*
606+
* @return void
607+
*/
608+
protected static function triggerSnakeCaseDynamicMethodDeprecation(string $name): void
609+
{
610+
$replacement = static::snakeCaseDynamicMethodToCamelCase($name);
611+
$message = 'Dynamic snake_case model methods are deprecated. Use ' . $replacement . ' instead of ' . $name . '.';
612+
trigger_error($message, E_USER_DEPRECATED);
613+
}
614+
615+
/**
616+
* Convert a snake_case dynamic method name to the camelCase replacement.
617+
*
618+
* @param string $name
619+
*
620+
* @return string
621+
*/
622+
protected static function snakeCaseDynamicMethodToCamelCase(string $name): string
623+
{
624+
$prefixMap = array(
625+
'findOne_by_' => 'findOneBy',
626+
'find_by_' => 'findBy',
627+
'first_by_' => 'firstBy',
628+
'last_by_' => 'lastBy',
629+
'count_by_' => 'countBy',
630+
);
631+
foreach ($prefixMap as $prefix => $replacementPrefix) {
632+
if (str_starts_with($name, $prefix)) {
633+
$fieldname = substr($name, strlen($prefix));
634+
return $replacementPrefix . static::snakeToStudly($fieldname);
635+
}
636+
}
637+
638+
return $name;
639+
}
640+
558641
/**
559642
* Resolve a dynamic field name from snake_case or CamelCase to an actual column name.
560643
*
@@ -599,6 +682,21 @@ protected static function camelToSnake($fieldname)
599682
return strtolower($snake ?? $fieldname);
600683
}
601684

685+
/**
686+
* Convert snake_case to StudlyCase for dynamic method generation.
687+
*
688+
* @param string $fieldname
689+
*
690+
* @return string
691+
*/
692+
protected static function snakeToStudly(string $fieldname): string
693+
{
694+
$parts = explode('_', $fieldname);
695+
$parts = array_map(static fn ($part) => ucfirst(strtolower($part)), $parts);
696+
697+
return implode('', $parts);
698+
}
699+
602700
/**
603701
* Count records for a field with either a single value or an array of values.
604702
*
@@ -618,9 +716,9 @@ protected static function countByField($fieldname, $match)
618716
/**
619717
* find one match based on a single field and match criteria
620718
*
621-
* @param string $fieldname
622-
* @param string|array $match
623-
* @param string $order ASC|DESC
719+
* @param string $fieldname
720+
* @param mixed $match
721+
* @param string $order ASC|DESC
624722
*
625723
* @return static|null object of calling class
626724
*/
@@ -637,8 +735,8 @@ public static function fetchOneWhereMatchingSingleField($fieldname, $match, $ord
637735
/**
638736
* find multiple matches based on a single field and match criteria
639737
*
640-
* @param string $fieldname
641-
* @param string|array $match
738+
* @param string $fieldname
739+
* @param mixed $match
642740
*
643741
* @return object[] of objects of calling class
644742
*/

test-src/Model/Category.php

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,13 @@
33
namespace App\Model;
44

55
/**
6-
* @method static array find_by_name($match)
7-
* @method static self|null findOne_by_name($match)
8-
* @method static self|null first_by_name($match)
9-
* @method static self|null last_by_name($match)
10-
* @method static int count_by_name($match)
116
* @method static array findByName($match)
127
* @method static self|null findOneByName($match)
138
* @method static self|null firstByName($match)
149
* @method static self|null lastByName($match)
1510
* @method static int countByName($match)
11+
* @method static self|null findOneByUpdatedAt($match)
12+
* Legacy snake_case dynamic methods remain temporarily supported and emit deprecation notices.
1613
* @property int|null $id primary key
1714
* @property string|null $name category name
1815
* @property string|null $updated_at mysql datetime string

0 commit comments

Comments
 (0)