Laravel DB Seeds - Test Data v Sample Data - php

I'm probably misunderstanding exactly how this works, but what's the best way to accomplish this? I have something in mind but it seems quite hacky.
I have a set of sample data which I use to test my application. This is seeded via the built in seeder in Laravel. This contains things like example users, addresses, documents etc.
I also have a set of default data which should go in production. I currently add this directly in the migration. For example, if I was adding a table for account_roles, I might include the following at the bottom of the migration
$account_admin = array('role' => 'Account Administrator', 'flag' => 'ACCOUNT_ADMIN');
$account_owner = array('role' => 'Account Administrator', 'flag' => 'ACCOUNT_OWNER');
DB::table('account_roles')->insert($account_admin);
DB::table('account_roles')->insert($account_owner);
This way, on production, I just migrate the database to insert any production ready database values, and on staging/development, I can refresh the migrations and then seed the database with sample data.
Is there any other (better) way to do this?

You could run a check on the current environment in your seeder file, and seed as needed
<?php
class DatabaseSeeder extends Seeder {
public function run()
{
Eloquent::unguard();
if (App::environment() === 'production')
{
$this->call('ProductionSeeder');
}
else
{
$this->call('StagingSeeder');
}
}
}

Related

CakePHP Unit tests are ignoring Fixtures/test databases and using application's data/databases

I have a legacy application which is running on CakePHP 4.3.8. Historically this application never had any unit tests but we're starting to add them as we work on new features.
The application uses 8 different MariaDB database connections. These are configured in config/app_local.php and all have their own key. There is a corresponding "test" database for each one prefixed with test_. As an example:
dev_notification_db: Local database for a system notifications feature of the app.
test_notification_db: Test database for the above
All database names follow this convention and have appropriate credentials and connection details to a local install of MariaDB 10.x. A schema with the appropriate name has been created locally and access has been granted to the user/pass referenced in the configuration. We have not had any connection or permissions errors.
In the dev_notification_db there are around 3400 rows of data.
I'm trying to write some unit tests following CakePHP's Testing docs. In this case I want test_notification_db to contain just 3 rows of data since for a particular test I'm writing I don't need all 3400 rows from the application database and certainly don't want them in a version controlled Fixture.
I have created a Fixture in tests/Fixture/NotificationsFixture.php which contains the 3 rows I want to add to the test database, test_notification_db. This file was created using the following command:
bin/cake bake fixture --connection dev_notification_db --conditions 1=1 --count 3400 --records Notifications
When the file was created it contained all 3400 rows of data from dev_notification_db. I manually removed all but the 3 of them that were necessary for testing purposes. The reason I did it in this way is because using the command above also reads the table structure and adds it to NotificationsFixture.php, meaning the table with the same columns/data types can be created on test_notification_db.
NotificationsFixture.php looks like this
class NotificationsFixture extends TestFixture
{
public $fields = [
// This is essentially the schema of the `notifications` table
// e.g. 'id' => ['type' => 'integer', 'length' => null, 'unsigned' => true, 'null' => false, 'default' => null, 'comment' => '', 'autoIncrement' => true, 'precision' => null]
// Other columns...
];
public function init(): void
{
$this->records = [
[
'id' => 1,
'column1' => 'foo',
'column2' => 'bar',
'column3' => 'baz',
],
// Other rows of test data...
}
}
In the init() method above, $this->records contains 3 rows of data which are the ones I want to use in my fixture.
According to the Creating Fixtures section of the CakePHP docs, it says
Fixtures defines the records that will be inserted into the test database at the beginning of each test.
Therefore my understanding is that the 3 rows in $this->records should end up in the test_notification_db when I'm running my tests.
There isn't anything else in the NotificationsFixture.php file, for example something telling it to use a different Data Source from the app_local.php config file: it has $fields and the init() method, and that's all.
My test for this is in tests/TestCase/Model/Table/NotificationsTest.php. One of my tests relies on it reading the Fixture data, i.e. the 3 rows from test_notification_db. In order to do this I've added the following to my test:
protected $fixtures = [
'app.Notifications',
// ...
];
public function setUp(): void
{
parent::setUp();
$this->Notifications = TableRegistry::getTableLocator()->get('Notifications');
}
public function testGetNotifications()
{
$userNotifications = $this->Notifications->find()->where(['user_id' => 1])->count();
debug($userNotifications);
die;
}
To explain my understanding of the code above:
The $fixtures array contains app.Notifications. This is consistent to what's shown in the CakePHP docs regarding Loading Fixtures in your Test Cases. It specficially says:
After you’ve created your fixtures, you’ll want to use them in your test cases. In each test case you should load the fixtures you will need. You should load a fixture for every model that will have a query run against it.
So my understanding is I'm loading the NotificationsFixture.php file referenced earlier.
The setUp() method gives me a reference to the Model that interacts with the notifications table, i.e. src/Model/Table/NotificationsTable.php. Given that this is being run in a Test Suite - and after reviewing the docs - this should be interacting with the test database (test_notification_db) NOT the application database (dev_notification_db).
The actual test uses the model in the point above and attempts to find notifications for a given user ID. The count in this case comes back as 400. In the application database there are 400 rows for the user ID given. However, in the fixture there are only 3 rows (all of which are for the user in question). Therefore this should come back as 3, not 400.
Upon debugging the full result set it's clear that the data is being loaded from dev_notification_db and not the test database, test_notification_db.
Why is this? If the purpose of the test suite is to be able to interact with the test database and the Fixtures are - quote - "records that will be inserted into the test database at the beginning of each test" then this is certainly not doing that.
What is missing here, or making it interact with the application's database and not the separate database for testing purposes?

CakePHP Migration Script doesn't update the Table Model

I need to add an admin column to my user table in my database. I created the migration script with the following command.
bin/cake bake migration AddAdminToUsers admin:boolean
This mostly did what I wanted, I just changed the default value to false. My Migration script now looks like this.
<?php
use Migrations\AbstractMigration;
class AddAdminToUsers extends AbstractMigration
{
public function change()
{
$table = $this->table('users');
$table->addColumn('admin', 'boolean', [
'default' => false,
'null' => false
]);
$table->update();
}
}
Also, oddly enough, I've tried this several times and each time I'm only able to run this migration script once. I have to delete it and re-bake a new one if I want another one to work.
When you run a migration it marks as migrated and you can not run it one more time unless do the rollback. Rollback will cancel previous migration and you will be able to run it one more time.Here is fully docs for plugin that cakphp using for migrations.

Create table with few rows with migration in laravel

I would like to create a table that will save the data for only 10 rows. How can I do it via migration in Laravel 5.3?
Laravel 5.3 provides seeding, also combined with model factories. I'm guessing you're using Eloquent models instead of query builder.
Model Factory
Here an example from Laravel (https://laravel.com/docs/5.3/seeding#using-model-factories)
factory(App\User::class, 10)->create();
This code creates 10 fake users via the User Eloquent model. The declaration of a fake user could be done in database/factories/ModelFactory.php.
Seeding
Again, a partial example from Laravel (https://laravel.com/docs/5.3/seeding#writing-seeders). You can call the model factory directly from the existing DatabaseSeeder (no need to create a new seeder).
<?php
use Illuminate\Database\Seeder;
use Illuminate\Database\Eloquent\Model;
class DatabaseSeeder extends Seeder
{
/**
* Run the database seeds.
*
* #return void
*/
public function run()
{
factory(App\User::class, 10)->create();
}
}
Run
php artisan db:seed to seed the data in existing table structure
php artisan migrate:refresh --seed for completely rebuilding your database and running the seeders
Full documentation and examples, see the provided links above.
There is no such restriction. You can create table as usual and just add 10 rows. If you're asking about hwo to add 10 rows to the table, read about seeding.
Also, if you have just 10 simple rows, consider using config file for that. You can do something like this:
'my-data' => [
1 => ['name' => 'John', 'age' => 30],
2 => ['name' => 'Alan', 'age' => 40],
....
],
And access this data with config('my-config.my-data')
If you want a dummy data to be filled into your table without any hassle you can take a use of Faker Package by fzaninotto.
The process would be like
Install faker via running below command in terminal (from project's root directory):
composer require fzaninotto/faker
Then in your routes.php you can push dummy entries like this:
// Just for an example I am using route to demonstrate the general use
Route::get('/customers',function(){
$faker = Faker\Factory::create();
$limit = 10; // this value sets number of rows to be created
// generate data by accessing properties
for ($i = 0; $i < $limit; $i++) {
User::create([
'name' => $faker->name,
'email' => $faker->email,
'phoneNumber' => $faker->phoneNumber,
]);
}
});
This would create total 10 rows in your table with some dummy data.. Hope this helps

Dynamically add columns in an existing table on the fly in CakePHP 3

I want to add column in my existing table in CakePHP 3.
My ContactsTable.php file code:
<?php
namespace App\Model\Table;
use Cake\ORM\Table;
use Migrations\AbstractMigration;
class ContactsTable extends Table
{
public function initialize(array $config)
{
$this->addBehavior('Timestamp');
$table = $this->table('contacts');
$table->addColumn('price', 'decimal')->update();
}
}
I have tried as described in CakePHP 3 documentation but I got this error:
Call to a member function addColumn() on a non-object
How do I add columns on-the-fly via the controller?
Code:
<?php
namespace App\Controller;
use Cake\Core\Configure;
use Cake\Network\Exception\NotFoundException;
use Cake\View\Exception\MissingTemplateException;
use Cake\ORM\TableRegistry;
use Cake\Database\Schema\Table;
use Cake\Datasource\ConnectionManager;
use \Migrations\AbstractMigration as AbstractMigration;
use \Phinx\Db\Adapter\MysqlAdapter as MysqlAdapter;
class PagesController extends AppController
{
public function display()
{
$connectionArray = ConnectionManager::get('default')->config();
$connectionArray['pass'] = $connectionArray['password'];
$connectionArray['user'] = $connectionArray['username'];
$connectionArray['name'] = $connectionArray['database'];
$migrationObject = new AbstractMigration(mt_rand());
$migrationObject->setAdapter(new MysqlAdapter($connectionArray));
$tree = $migrationObject->table('tests');
$tree->addColumn('something', 'text')
->update();
}
}
After few hours of Hacking, finally found a way to do it on-the-fly.
Tested in default cakephp 3 (latest - as of today - 2nd June '16)
If you are using a different database adapter, change it to that adapater from MysqlAdapter.
Note to the users:
This is an ugly hack and should be used ONLY if you do not work in
an organization where each migration commit requires peer reference.
mt_rand() must NEVER be used as a version number hack.
There is no canonical way of doing it via the controllers. Update in a datasource MUST always be done modified via migrations - using a proper structure.
Refer to Running Migrations in a non-shell environment and try to create a migrations logs under /config/migrations, that would be more rule-specific-on-the-fly and you will also have logs for peers to review.
Migration plugin also support Running Migrations in a non-shell environment.
Since the release of version 1.2 of the migrations plugin, you can run migrations from a non-shell environment, directly from an app, by using the new Migrations class. This can be handy in case you are developing a plugin installer for a CMS for instance. The Migrations class allows you to run the following commands from the migrations shell: migrate, rollback, markMigrated, status and seed.
Each of these commands has a method defined in the Migrations class.
You can prepare some custom handler which will accept column data from user side and run migration. In this case it could be some form with name and type inputs. Migration will be applied to DB after form with data will be submitted.
Here is how to use it:
use Migrations\Migrations;
$migrations = new Migrations();
// Will return an array of all migrations and their status
$status = $migrations->status();
// Will return true if success. If an error occurred, an exception will be thrown
$migrate = $migrations->migrate();
// Will return true if success. If an error occurred, an exception will be thrown
$rollback = $migrations->rollback();
// Will return true if success. If an error occurred, an exception will be thrown
$markMigrated = $migrations->markMigrated(20150804222900);
// Will return true if success. If an error occurred, an exception will be thrown
$seeded = $migrations->seed();
If you want add new column to product table e.g 'price' and price is a 'decimal' you should go to your project and write this in console:
bin/cake bake migration AddPriceToProducts price:decimal
You can see a new file e.g. Config/Migrations/20160501190410_AddPriceToProducts.php
<?php
use Migrations\AbstractMigration;
class AddPriceToProducts extends AbstractMigration
{
/**
* Change Method.
*
* More information on this method is available here:
* http://docs.phinx.org/en/latest/migrations.html#the-change-method
* #return void
*/
public function change()
{
$table = $this->table('products');
$table->addColumn('price', 'decimal', [
'default' => null,
...
'null' => true,
]);
$table->update();
}
}
and later just launch migrations to add this column to data base, write this in console:
bin/cake migrations migrate

Laravel migration transaction

When developing i'm having so many issues with migrations in laravel.
I create a migration. When i finish creating it, there's a small error by the middle of the migration (say, a foreign key constraint) that makes "php artisan migrate" fail. He tells me where the error is, indeed, but then migrate gets to an unconsistent state, where all the modifications to the database made before the error are made, and not the next ones.
This makes that when I fix the error and re-run migrate, the first statement fails, as the column/table is already created/modified. Then the only solution I know is to go to my database and "rollback" everything by hand, which is way longer to do.
migrate:rollback tries to rollback the previous migrations, as the current was not applied succesfully.
I also tried to wrap all my code into a DB::transaction(), but it still doesn't work.
Is there any solution for this? Or i just have to keep rolling things back by hand?
edit, adding an example (not writing Schema builder code, just some kind of pseudo-code):
Migration1:
Create Table users (id, name, last_name, email)
Migration1 executed OK. Some days later we make Migration 2:
Create Table items (id, user_id references users.id)
Alter Table users make_some_error_here
Now what will happen is that migrate will call the first statement and will create the table items with his foreign key to users. Then when he tries to apply the next statement it will fail.
If we fix the make_some_error_here, we can't run migrate because the table "items" it's created. We can't rollback (nor refresh, nor reset), because we can't delete the table users since there's a foreign key constraint from the table items.
Then the only way to continue is to go to the database and delete the table items by hand, to get migrate in a consistent state.
It is not a Laravel limitation, I bet you use MYSQL, right?
As MYSQL documentation says here
Some statements cannot be rolled back. In general, these include data
definition language (DDL) statements, such as those that create or
drop databases, those that create, drop, or alter tables or stored
routines.
And we have a recommendation of Taylor Otwell himself here saying:
My best advice is to do a single operation per migration so that your
migrations stay very granular.
-- UPDATE --
Do not worry!
The best practices say:
You should never make a breaking change.
It means, in one deployment you create new tables and fields and deploy a new release that uses them. In a next deployment, you delete unused tables and fields.
Now, even if you'll get a problem in either of these deployments, don't worry if your migration failed, the working release uses the functional data structure anyway. And with the single operation per migration, you'll find a problem in no time.
I'm using MySql and I'm having this problem.
My solution depends that your down() method does exactly what you do in the up() but backwards.
This is what i go:
try{
Schema::create('table1', function (Blueprint $table) {
//...
});
Schema::create('tabla2', function (Blueprint $table) {
//...
});
}catch(PDOException $ex){
$this->down();
throw $ex;
}
So here if something fails automatically calls the down() method and throws again the exception.
Instead of using the migration between transaction() do it between this try
Like Yevgeniy Afanasyev highlighted Taylor Otwell as saying (but an approach I already took myself): have your migrations only work on specific tables or do a specific operation such as adding/removing a column or key. That way, when you get failed migrations that cause inconsistent states like this, you can just drop the table and attempt the migration again.
I’ve experienced exactly the issue you’ve described, but as of yet haven’t found a way around it.
Just remove the failed code from the migration file and generate a new migration for the failed statement. Now when it fails again the creation of the database is still intact because it lives in another migration file.
Another advantage of using this approach is, that you have more control and smaller steps while reverting the DB.
Hope that helps :D
I think the best way to do it is like shown in the documentation:
DB::transaction(function () {
DB::table('users')->update(['votes' => 1]);
DB::table('posts')->delete();
});
See: https://laravel.com/docs/5.8/database#database-transactions
I know it's an old topic, but there was activity a month ago, so here are my 2 cents.
This answer is for MySql 8 and Laravel 5.8
MySql, since MySql 8, introduced atomic DDL: https://dev.mysql.com/doc/refman/8.0/en/atomic-ddl.html
Laravel at the start of migration checks if the schema grammar supports migrations in a transaction and if it does starts it as such.
The problem is that the MySql schema grammar has it set to false. We can extend the Migrator, MySql schema grammar and MigrationServiceProvider, and register the service provider like so:
<?php
namespace App\Console;
use Illuminate\Database\Migrations\Migrator as BaseMigrator;
use App\Database\Schema\Grammars\MySqlGrammar;
class Migrator extends BaseMigrator {
protected function getSchemaGrammar( $connection ) {
if ( get_class( $connection ) === 'Illuminate\Database\MySqlConnection' ) {
$connection->setSchemaGrammar( new MySqlGrammar );
}
if ( is_null( $grammar = $connection->getSchemaGrammar() ) ) {
$connection->useDefaultSchemaGrammar();
$grammar = $connection->getSchemaGrammar();
}
return $grammar;
}
}
<?php
namespace App\Database\Schema\Grammars;
use Illuminate\Database\Schema\Grammars\MySqlGrammar as BaseMySqlGrammar;
class MySqlGrammar extends BaseMySqlGrammar {
public function __construct() {
$this->transactions = config( "database.transactions", false );
}
}
<?php
namespace App\Providers;
use Illuminate\Database\MigrationServiceProvider as BaseMigrationServiceProvider;
use App\Console\Migrator;
class MigrationServiceProvider extends BaseMigrationServiceProvider {
/**
* Register the migrator service.
* #return void
*/
protected function registerMigrator() {
$this->app->singleton( 'migrator', function( $app ) {
return new Migrator( $app[ 'migration.repository' ], $app[ 'db' ], $app[ 'files' ] );
} );
$this->app->singleton(\Illuminate\Database\Migrations\Migrator::class, function ( $app ) {
return $app[ 'migrator' ];
} );
}
<?php
return [
'providers' => [
/*
* Laravel Framework Service Providers...
*/
App\Providers\MigrationServiceProvider::class,
],
];
Of course, we have to add transactions to our database config...
DISCLAIMER - Haven't tested yet, but looking only at the code it should work as advertised :) Update to follow when I test...
Most of the answers overlook a very important fact about a very simple way to structure your development against this. If one were to make all migrations reversible and add as much of the dev testing data as possible through seeders, then when artisan migrate fails on the dev environment one can correct the error and then do
php artisan migrate:fresh --seed
Optionally coupled with a :rollback to test rolling back.
For me personally artisan migrate:fresh --seed is the second most used artisan command after artisan tinker.

Categories