tobento / app-seeding
App seeding support.
Requires
- php: >=8.0
- tobento/app: ^1.0.7
- tobento/app-console: ^1.0
- tobento/app-migration: ^1.0
- tobento/service-iterable: ^1.0
- tobento/service-seeder: ^1.0.1
- tobento/service-support: ^1.0
Requires (Dev)
- mockery/mockery: ^1.6
- phpunit/phpunit: ^9.5
- tobento/app-user: ^1.0
- vimeo/psalm: ^4.0
README
Seeding support for the app using the Seeder Service.
Table of Contents
Getting Started
Add the latest version of the app seeding project running this command.
composer require tobento/app-seeding
Requirements
- PHP 8.0 or greater
Documentation
App
Check out the App Skeleton if you are using the skeleton.
You may also check out the App to learn more about the app in general.
Seeding Boot
The seeding boot does the following:
- SeedInterface implementation
- SeedersInterface implementation
- adds console commands for seeding
The following seeders will be available:
Keep in mind that no Resources are set as they may be specific to your app needs. Therefore, the seeders mostly using the Lorem Seeder as fallback.
use Tobento\App\AppFactory; use Tobento\Service\Seeder\SeedInterface; use Tobento\App\Seeding\SeedersInterface; // Create the app $app = (new AppFactory())->createApp(); // Add directories: $app->dirs() ->dir(realpath(__DIR__.'/../'), 'root') ->dir(realpath(__DIR__.'/../app/'), 'app') ->dir($app->dir('app').'config', 'config', group: 'config') ->dir($app->dir('root').'vendor', 'vendor'); // Adding boots $app->boot(\Tobento\App\Seeding\Boot\Seeding::class); $app->booting(); // Available Interfaces: $seed = $app->get(SeedInterface::class); $seeders = $app->get(SeedersInterface::class); // Run the app $app->run();
Adding Seed Resources
You may add seeder resources by the following ways:
Globally by using the app on
method
use Tobento\App\AppFactory; use Tobento\Service\Seeder\SeedInterface; use Tobento\Service\Seeder\Resource; // Create the app $app = (new AppFactory())->createApp(); // Add directories: $app->dirs() ->dir(realpath(__DIR__.'/../'), 'root') ->dir(realpath(__DIR__.'/../app/'), 'app') ->dir($app->dir('app').'config', 'config', group: 'config') ->dir($app->dir('root').'vendor', 'vendor'); // Adding boots $app->boot(\Tobento\App\Seeding\Boot\Seeding::class); $app->booting(); // Add resources: $app->on(SeedInterface::class, function(SeedInterface $seed) { $seed->resources()->add(new Resource('countries', 'en', [ 'Usa', 'Switzerland', 'Germany', ])); }); $seed = $app->get(SeedInterface::class); var_dump($seed->country()); // string(7) "Germany" // Run the app $app->run();
Specific on any service using the seed
use Tobento\Service\Seeder\SeedInterface; use Tobento\Service\Seeder\Resource; class ServiceUsingSeed { public function __construct( protected SeedInterface $seed, ) { $seed->resources()->add(new Resource('countries', 'en', [ 'Usa', 'Switzerland', 'Germany', ])); } }
Factories
Creating Factories
You may create seed factories for testing or other purposes.
To create a factory, create a class that extends the AbstractFactory::class
and configure your entity by using the definition
method:
use Tobento\App\Seeding\AbstractFactory; class UserFactory extends AbstractFactory { public function definition(): array { return [ 'firstname' => $this->seed->firstname(), 'role' => 'admin', ]; } }
Creating entities
You may use the createEntity
method to create specific entites using the definition. By default, a stdClass
class will be created.
use Tobento\App\Seeding\AbstractFactory; class UserFactory extends AbstractFactory { protected function createEntity(array $definition): object { return new User( firstname: $definition['firstname'], ); } }
Storing entities
By default, entities will not be stored. You may use the storeEntity
method to store the entity based on the definition. In Addition, you may use getService
method to get any service from the app container.
use Tobento\App\Seeding\AbstractFactory; class UserFactory extends AbstractFactory { protected function storeEntity(array $definition): object { return $this->getService(UserRepositoryInterface::class) ->create($definition); } }
Using Factories
A factory can be created by calling the new
method on the factory class:
$factory = UserFactory::new(); // you may overwrite the default definition: $factory = UserFactory::new(['role' => 'editor']);
Make entities
The make
method creates an array of entities and returns them for further use in code, but does not store them in the database e.g.
// make 10 entities: $users = $factory->times(10)->make(); // make one: $user = $factory->makeOne();
Create entities
The create
method creates an array of entities, stores them in the database for instance and returns them for further use in the code.
// create 10 entities: $users = $factory->times(10)->create(); // create one: $user = $factory->createOne();
Raw entities
The raw
method creates an array of attributes from definition only and returns them for further use in code.
// create 10: $attributes = $factory->times(10)->raw(); // create one: $attributes = $factory->rawOne();
Modify definition
You may modify the definition by using the modify
method:
use Tobento\Service\Seeder\SeedInterface; $users = $factory ->modify(fn (SeedInterface $seed, array $definition) => [ 'email' => $seed->email(), ]) ->times(10) ->make();
You may use the method inside your factory class:
use Tobento\App\Seeding\AbstractFactory; use Tobento\Service\Seeder\SeedInterface; class UserFactory extends AbstractFactory { public function withEmail(string $email): static { return $this->modify(fn(SeedInterface $seed, array $definition) => [ 'email' => $email ]); } } $users = $factory ->withEmail('admin@example.com') ->times(10) ->make();
Modify entity
You may modify the entity by using the modifyEntity
method:
use Tobento\Service\Seeder\SeedInterface; $users = $factory ->modifyEntity(static function (SeedInterface $seed, object $user) { return $user->markAsDeleted(); }) ->times(10) ->make();
You may use the method inside your factory class:
use Tobento\App\Seeding\AbstractFactory; use Tobento\Service\Seeder\SeedInterface; class UserFactory extends AbstractFactory { public function deleted(string $email): static { return $this->modifyEntity(static function (SeedInterface $seed, object $user) { return $user->markAsDeleted(); }); } } $users = $factory ->deleted() ->times(10) ->make();
Seeders
You may create a seeder class to easily seed your application with test data.
Creating Seeders
To create a seeder, create a class that implements the SeederInterface::class
:
use Tobento\App\Seeding\SeederInterface; class UserSeeder implements SeederInterface { public function run(): \Generator { foreach (UserFactory::new()->times(100)->create() as $user) { yield $user; } } }
If you want to seed millions of test data, using factories my not be the fastest solution. Instead you may directly use the storage if available which is much faster.
Example using the user repository from the App User bundle:
use Tobento\App\Seeding\SeederInterface; use Tobento\App\User\UserRepositoryInterface; use Tobento\Service\Repository\Storage\StorageRepository; use Tobento\Service\Iterable\ItemFactoryIterator; class UserSeeder implements SeederInterface { public function __construct( protected UserRepositoryInterface $userRepository, ) { if (! $userRepository instanceof StorageRepository) { throw new \InvalidArgumentException('Not supported ...'); } } public function run(): \Generator { yield from $this->userRepository ->query() ->chunk(length: 10000) ->insertItems(new ItemFactoryIterator( factory: function (): array { return UserFactory::new()->definition(); }, create: 1000000, )); } }
Adding Seeders
You may add seeders to be run by the app console using the app on
method.
use Tobento\App\Seeding\SeedersInterface; // ... $app->on( SeedersInterface::class, static function (SeedersInterface $seeders): void { $seeders->addSeeder('users', UserSeeder::class); } );
Running Seeders
To run your added seeders use the seed
console command.
php ap seed
Run only specific seeders by its name
php ap seed --name=users
Display seeded entities
You may display the seeded entities with the verbosity option:
php ap seed -v
List all seeder names
You may display the seeder names by using the seed:list
console command.
php ap seed:list
User Seeding
If you have installed the User App bundle you may use the provided user factory and user seeder.
User Factory
use Tobento\App\Seeding\User\UserFactory; $user = UserFactory::new() ->withEmail('foo@example.com') ->withSmartphone('22334455') ->withUsername('Username') ->withPassword('123456') ->withRoleKey('admin') // 'guest' if role does not exist. ->withAddress(['firstname' => 'Firstname']) ->makeOne();
User Seeder
use Tobento\App\Seeding\User\UserStorageSeeder; $app->on( SeedersInterface::class, static function (SeedersInterface $seeders): void { $seeders->addSeeder('users', UserStorageSeeder::class); } );
Repository
You may easily create seed factories and seeders from any repository implementing the Repository Interface.
Creating Repository Factories
Using the repository factory
By using the RepositoryFactory::new
method, you can quickly create a seed factory, which may be useful for testing purposes.
use Tobento\App\Seeding\Repository\RepositoryFactory; use Tobento\Service\Repository\RepositoryInterface; use Tobento\Service\Seeder\SeedInterface; use Tobento\Service\Seeder\Lorem; $factory = RepositoryFactory::new( repository: MyProductRepository::class, // string|RepositoryInterface definition: function (SeedInterface $seed): array { return [ 'sku' => Lorem::word(number: 1), 'desc' => Lorem::sentence(number: 2), ]; } ); // using the factory: $products = $factory->times(10)->make();
Storage Repositories with defined columns, will create the definition automatically based on the columns. You do not need to set a definition at all.
use Tobento\App\Seeding\Repository\RepositoryFactory; $factory = RepositoryFactory::new(repository: MyProductRepository::class); // using the factory: $products = $factory->times(10)->make();
Check out the Using Factories section for more info using the factory in general.
Using the AbstractFactory
To create a factory, create a class that extends the AbstractFactory::class
and use the REPOSITORY
constant to define your repository class:
use Tobento\App\Seeding\Repository\AbstractFactory; class ProductFactory extends AbstractFactory { public const REPOSITORY = MyProductRepository::class; } // using the factory: $products = ProductFactory::new()->times(10)->make();
Sure, you may customize the definition for more flexibility:
use Tobento\App\Seeding\Repository\AbstractFactory; class ProductFactory extends AbstractFactory { public function definition(): array { // you may call the parent if you have // repositories with columns so it will // automatically create the definition // based on the columns. $definition = parent::definition(); // and just overwrite if needed: $definition['name'] = $this->seed->firstname(); return $definition; } }