@@ -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 */
0 commit comments