efabrica / nette-repository
Repositories for Nette database
Installs: 15 524
Dependents: 0
Suggesters: 0
Security: 0
Stars: 1
Watchers: 3
Forks: 2
Open Issues: 0
Requires
- php: ^8.1
- ext-json: *
- ext-pdo: *
- doctrine/inflector: ^2.0
- nette/caching: ^3.1
- nette/database: ^3.1 <3.2
- nette/di: ^3.1
- nette/php-generator: ^4.1.4
- nikic/php-parser: ^4.16|^5.0
- symfony/console: ^5.4|^6.0|^7.0
- symfony/polyfill-php80: ^1.29
Requires (Dev)
- nesbot/carbon: ^3.1
- nette/application: ^3.1
- nette/bootstrap: ^3.1
- nette/http: ^3.2
- phpunit/phpunit: ^9.6
- ramsey/uuid: ^4.2
- tracy/tracy: ^2.9
This package is auto-updated.
Last update: 2024-10-22 09:30:41 UTC
README
This extension enhances the static analysis of your Nette Database by providing typehinted entities (ActiveRows), queries (Selections) and repositories (services).
Installation
You can install this extension using Composer, the dependency manager for PHP:
composer require efabrica/nette-repository
To enable the extension, you need to register it in config:
extensions: netteRepo: Efabrica\NetteRepository\Bridge\EfabricaNetteRepositoryExtension
Finally, you can run the repository code generation command to generate the necessary classes and files:
$ php vendor/bin/enc
Usage
Entity
The Entity
class is a subclass of ActiveRow that serves as a superclass for your entities.
You can either use our code generation tool to create it automatically or write it manually.
Inserting an entity
assert($repository instanceof PersonRepository); assert($entity instanceof Person); $entity = $repository->create(); $entity->name = 'John'; $entity->surname = 'Doe'; $entity->age = 42; $entity->save();
Save array:
$entity = $repository->insertOne([ Person::NAME => 'John', Person::SURNAME => 'Doe', Person::AGE => 42, ]); // always returns entity // More verbose approach: $entity = $repository->create(); $entity->fill([ Person::NAME => 'John', Person::SURNAME => 'Doe', Person::AGE => 42, ]); $entity->save();
Classic:
$person = $repository->insert([ Person::NAME => 'John', Person::SURNAME => 'Doe', Person::AGE => 42, ]); // always returns Entity
Multi-insert:
$persons = []; foreach (range(30, 40) as $age) { $person = $repository->createRow(); Person::NAME => 'John', Person::SURNAME => 'Doe', Person::AGE => $age, $persons[] = $person; } $repository->insertMany($persons); // always returns int
Updating an entity
$entity = $repository->find($id); $entity->name = 'Jake'; $entity->update(); // or $entity->save();
Classic:
$repository->update($id, [Person::name => 'Jake']);
Deleting an entity
$repository->delete($id);
Or, if you already have the entity available:
$entity = $repository->find($id); $entity->delete();
Scopes
Scope is a class that defines which existing behaviors are disabled for the Repository, Query and Entity.
The active Scope is passed down from Repository to Query and from Query down to Entity.
->withScope(Scope $scope)
returns a clone of the object with the given scope applied.
->scopeRaw()
returns a clone of the object with raw scope. Raw scope removes all behaviors.
->scopeFull()
returns a clone of the object with full scope. Full scope keeps all behaviors.
This is the default scope, unless you change the scope in repository's setup() method.
Example
final class AdminScope implements \Efabrica\NetteRepository\Repository\Scope\Scope { public function apply(RepositoryBehaviors $behaviors, Repository $repository): void { // Remove these behaviors because they are not needed for the Admin $behaviors ->remove(SoftDeleteBehavior::class) ->remove(PublishBehavior::class); // Do not add any new behaviors here, because this scope can be used by different repositories // and you might introduce unwanted side effects. // However, you can conditionally add behaviors based on the repository type and some parameter // For example, if you want to apply a special behavior for admin users in the user repository, you can do this: if ($repository instanceof UserRepository && $someContainerParameter) { // Scopes can be services and receive parameters. $behaviors->add(AdminBehavior::class); // This is a hypothetical behavior, just for illustration. } } }
To use the Scope as a container service, which may not be necessary in your case, please follow these steps:
use Efabrica\NetteRepository\Repository\RepositoryBehaviors; abstract class RepositoryBase extends \Efabrica\NetteRepository\Repository\Repository { /** @inject */ public AdminScope $adminScope; public function scopeAdmin(): self { return $this->withScope($this->adminScope); } // Do this if you want to set the AdminScope as default: protected function setup(RepositoryBehaviors $behaviors) : void { $behaviors->setScope($this->adminScope); } }
And optionally implement shorthand methods for your queries:
use YourBeautifulApplication\Admin\AdminScope; abstract class QueryBase extends \Efabrica\NetteRepository\Repository\Query { public function scopeAdmin(): self { // This method returns a copy of the query with your own Admin scope applied // The Admin scope removes some behaviors that are not relevant for the Admin return $this->withScope($this->repository->adminScope); // Alternatively, if the Admin scope does not depend on any parameters, you can create a new instance of it like this: return $this->withScope(new AdminScope()); } }
Usage:
// all of these are equivalent: $repository->findBy(['age > ?' => 18])->scopeAdmin()->fetchAll(); $repository->scopeAdmin()->findBy(['age > ?' => 18])->fetchAll(); $repository->query()->where('age > ?', 18)->scopeAdmin()->fetchAll(); $repository->scopeAdmin()->query()->where('age > ?', 18)->fetchAll(); $repository->query()->scopeAdmin()->where('age > ?', 18)->fetchAll();
Code Generator
Code generation is fully optional, but it is recommended to use it.
To run the code generation, use this command:
$ php bin/console efabrica:nette-repo:code-gen # OR $ php bin/console e:n:c # OR $ php vendor/bin/enc
For every table in the database, it will generate these classes in the /Generated/
namespace: (Example: person
table)
Repository\Generated\Repository\PersonRepositoryBase
- Repository base class, holds typehints forPersonQuery
andPerson
entity. ( abstract)Repository\Generated\Query\PersonQuery
- Query class, holds typehints forPerson
entity. (abstract)Repository\Generated\Entity\Person
- Entity class, holds types for columns and public constants for column names. (final)
These classes are always regenerated when you run the code generator. They should not be modified manually.
For every table in the database, it will also generate these classes outside of the /Generated/
namespace, but only if they don't
exist. If they exist, they will not be overwritten:
Repository\PersonRepository
- extendsPersonRepositoryBase
. Here you write your custom repository methods. (final)Repository\Query\PersonQuery
- extendsPersonQuery
. Here you write your custom query methods. (final)Repository\Entity\PersonBody
- trait that is inserted intoPerson
entity. Here you write your custom entity methods. (trait)
These classes are not regenerated when you run the code generator. They are meant to be customized by you. If you want to regenerate them, you have to delete them first.
Ignoring tables
It is also possible to ignore some tables. To do that, you can modify the ignoredTables
parameter in the config file:
netteRepo:
ignoreTables:
# These are the defaultly ignored tables:
migrations: true
migration_log: true
phoenix_log: true
phinxlog: true
Custom Inheritance
If you want to set different extends
or implements
for a generated class, you can do that by adding an entry into your config file:
netteRepo: inheritance: AuthorRepositoryBase: extends: 'App\Repository\PeopleRepositoryBase' implements: ['App\Repository\PersonRepositoryInterface'] AuthorQuery: extends: 'App\Repository\PeopleQueryBase' Person: implements: ['App\Repository\PersonInterface']
- Every generated class can be used for this.
- Key is the short class name (without namespace). Generated classes are made such that there are no namespace collisions, so this shouldn't prove a problem.
extends
must be string or null/not specified.implements
must be string array or empty array or null/not specified.- You cannot unimplement an interface.
This config schema is a bit verbose, but very intuitive once you see it and easy to read.
Behaviors and Traits
DateBehavior
This behavior automatically sets the created_at
and updated_at
columns to the current date and time when inserting or updating a row.
FilterBehavior
This behavior applies a default where() condition to every select query.
KeepDefaultBehavior
This behavior ensures that there is always at least one row with a truthy value in the default column. This is useful for flag columns.
SoftDeleteBehavior
This behavior marks a row as deleted by setting the deleted_at
column to the current date and time instead of removing it from the table.
LastManStandingBehavior
This behavior prevents deleting the last row in the table that matches a given query.
TreeTraverseBehavior
This behavior manages the lft
and rgt
columns that represent the hierarchical structure of the table. It updates them automatically when
inserting or updating a row.
SortingTrait
This trait adds methods to the repository for changing the order of rows, such as moveUp(), moveDown(), insertBefore(), and insertAfter().
CastBehavior
This behavior automatically casts values to the specified type when retrieving them from the database.
There are some predefined casts, but you can also define your own.
JsonCastBehavior
: casts values from JSON to PHP array and vice versa.CarbonCastBehavior
: casts values from MySQL datetime to CarbonImmutable and vice versa.
Events
There are several events that you can listen to in your repository: Insert, Update, Delete, Select, Load
To implement your own event subscriber, create a new class that extends Efabrica\NetteRepository\Subscriber\EventSubscriber
and register
it in the container. It will get automatically detected, since it extends the EventSubscriber.