Repositories

PointArt's repository pattern mirrors Spring Data JPA. Extend Repository, declare abstract methods, and the framework generates the implementation at runtime.

Namespaces PointStart\ORM — Repository
PointStart\Attributes — Query

Defining a Repository

Place repository classes in app/repositories/. Declare the class as abstract and set $entityClass:

abstract class UserRepository extends Repository {
    protected string $entityClass = User::class;

    // Custom SQL via #[Query]
    #[Query("SELECT * FROM users WHERE name = ? AND email = ?")]
    abstract public function findByNameAndEmailRaw(string $name, string $email): array;

    #[Query("SELECT COUNT(*) FROM users")]
    abstract public function countAll(): int;

    // Dynamic finder — no body needed
    abstract public function findByName(string $name): array;
}
Abstract Required Repository subclasses must be declared abstract. The framework calls Repository::make() at runtime to generate a concrete implementation class (UserRepository__Impl) via eval().

Built-in Methods

Every repository inherits these methods from the base Repository class:

MethodDescription
find($id)Find entity by primary key
findAll()Return all entities
save($entity)Insert or update an entity
delete($entity)Delete an entity
deleteById($id)Delete by primary key

#[Query] — Custom SQL

Use #[Query] to attach raw SQL to an abstract method. Parameters are bound positionally in method signature order:

#[Query("SELECT * FROM users WHERE name = ? AND email = ?")]
abstract public function findByNameAndEmailRaw(string $name, string $email): array;

#[Query("SELECT COUNT(*) FROM users")]
abstract public function countAll(): int;
ParameterTypeRequiredDescription
queryStringstringYesRaw SQL to execute. Use ? for positional parameters

The return type drives the result shape:

Return TypeBehaviour
arrayFetch all rows, map to entity instances
intScalar fetch (e.g. COUNT(*))
voidExecute only (e.g. INSERT, UPDATE, DELETE)

Dynamic Finders

Declare abstract methods and the framework generates the query from the method name — no implementation required:

MethodGenerated SQL
findByName($n)WHERE name = ?
findByNameAndEmail($n, $e)WHERE name = ? AND email = ?
findByAgeGreaterThan($age)WHERE age > ?
findByNameOrderByEmail($n)WHERE name = ? ORDER BY email
findOneByEmail($e)WHERE email = ? LIMIT 1
countByStatus($s)SELECT COUNT(*) WHERE status = ?
existsByEmail($e)Returns bool
deleteByStatus($s)DELETE WHERE status = ?

Operator Suffixes

Append these suffixes to field names in dynamic finders to control the comparison operator:

SuffixSQL OperatorExample
(none)= ?findByName($n)
GreaterThan> ?findByAgeGreaterThan($age)
LessThan< ?findByPriceLessThan($max)
GreaterThanEqual>= ?findByScoreGreaterThanEqual($min)
LessThanEqual<= ?findByStockLessThanEqual($threshold)
Not!= ?findByStatusNot($status)
LikeLIKE ?findByNameLike($pattern)
IsNullIS NULLfindByDeletedAtIsNull() (no arg)
IsNotNullIS NOT NULLfindByEmailIsNotNull() (no arg)

Full Example

abstract class ProductRepository extends Repository {
    protected string $entityClass = Product::class;

    // Custom SQL
    #[Query("SELECT * FROM products WHERE active = 1")]
    abstract public function findAllActive(): array;

    #[Query("SELECT * FROM products WHERE price <= ?")]
    abstract public function findAffordable(float $maxPrice): array;

    // Dynamic finders
    abstract public function findByName(string $name): array;
    abstract public function findByStockLessThan(int $threshold): array;

    // Count and existence checks
    abstract public function countByActive(bool $active): int;
    abstract public function existsByName(string $name): bool;

    // Delete
    abstract public function deleteByActive(bool $active): void;
}

Next: Views →