elabftw/elabftw

View on GitHub
src/commands/PopulateDatabase.php

Summary

Maintainability
A
0 mins
Test Coverage
<?php

/**
 * @author Nicolas CARPi <nico-git@deltablot.email>
 * @copyright 2012 Nicolas CARPi
 * @see https://www.elabftw.net Official website
 * @license AGPL-3.0
 * @package elabftw
 */

declare(strict_types=1);

namespace Elabftw\Commands;

use Elabftw\Elabftw\Db;
use Elabftw\Elabftw\Sql;
use Elabftw\Enums\Action;
use Elabftw\Enums\BasePermissions;
use Elabftw\Exceptions\ImproperActionException;
use Elabftw\Models\Config;
use Elabftw\Models\Experiments;
use Elabftw\Models\ExperimentsCategories;
use Elabftw\Models\ExperimentsLinks;
use Elabftw\Models\Items;
use Elabftw\Models\ItemsLinks;
use Elabftw\Models\ItemsStatus;
use Elabftw\Models\ItemsTypes;
use Elabftw\Models\Teams;
use Elabftw\Models\Templates;
use Elabftw\Models\Users;
use Elabftw\Services\Populate;
use League\Flysystem\Filesystem as Fs;
use League\Flysystem\Local\LocalFilesystemAdapter;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\ConfirmationQuestion;
use Symfony\Component\Yaml\Exception\ParseException;
use Symfony\Component\Yaml\Yaml;

use function array_key_exists;
use function is_string;
use function mb_strlen;
use function str_repeat;

/**
 * Populate the database with example data. Useful to get a fresh dev env.
 * For dev purposes, should not be used by normal users.
 */
#[AsCommand(name: 'db:populate')]
class PopulateDatabase extends Command
{
    // number of things to create
    private const int DEFAULT_ITERATIONS = 50;

    protected function configure(): void
    {
        $this->setDescription('Populate the database with fake data')
            ->addOption('yes', 'y', InputOption::VALUE_NONE, 'Skip confirmation question')
            ->addArgument('file', InputArgument::REQUIRED, 'Yaml configuration file')
            ->setHelp('This command allows you to populate the database with fake users/experiments/items. The database will be dropped before populating it. The configuration is read from the yaml file passed as first argument.');
    }

    protected function execute(InputInterface $input, OutputInterface $output)
    {
        try {
            $file = $input->getArgument('file');
            if (!is_string($file)) {
                throw new ImproperActionException('Could not read file from provided file path!');
            }
            $yaml = Yaml::parseFile($file);
        } catch (ParseException | ImproperActionException $e) {
            $output->writeln(sprintf('Error parsing the file: %s', $e->getMessage()));
            return 1;
        }

        // display header
        $output->writeln(array(
            $this->getDescription(),
            str_repeat('=', mb_strlen($this->getDescription())),
        ));

        // ask confirmation before deleting all the database
        $helper = $this->getHelper('question');
        // the -y flag overrides the config value
        if ($yaml['skip_confirm'] === false && !$input->getOption('yes')) {
            $question = new ConfirmationQuestion("WARNING: this command will completely ERASE your current database!\nAre you sure you want to continue? (y/n)\n", false);

            /** @phpstan-ignore-next-line ask method is part of QuestionHelper which extends HelperInterface */
            if (!$helper->ask($input, $output, $question)) {
                $output->writeln('Aborting!');
                return 1;
            }
        }

        // drop database
        $output->writeln('Dropping current database and loading structure...');
        $this->dropAndInitDb();

        // adjust global config
        $Config = Config::getConfig();
        $Config->patch(Action::Update, $yaml['config'] ?? array());

        $output->writeln('Creating teams, users, experiments, and resources...');
        // create teams
        $Users = new Users();
        $Teams = new Teams($Users);
        $Teams->bypassReadPermission = true;
        $Teams->bypassWritePermission = true;
        $Status = new ItemsStatus($Teams);
        $Category = new ExperimentsCategories($Teams);
        $faker = \Faker\Factory::create();

        foreach ($yaml['teams'] as $team) {
            $id = $Teams->postAction(Action::Create, array('name' => $team['name'], 'default_category_name' => $team['default_category_name'] ?? 'Lorem ipsum'));
            $Teams->setId($id);

            // create fake categories and status
            $Category->postAction(Action::Create, array('name' => 'Cell biology', 'color' => $faker->hexColor(), 'is_default' => 0));
            $Category->postAction(Action::Create, array('name' => 'Project CRYPTO-COOL', 'color' => $faker->hexColor(), 'is_default' => 0));
            $Category->postAction(Action::Create, array('name' => 'Project CASIMIR', 'color' => $faker->hexColor(), 'is_default' => 0));
            $Category->postAction(Action::Create, array('name' => 'Tests', 'color' => $faker->hexColor(), 'is_default' => 0));
            $Category->postAction(Action::Create, array('name' => 'Demo', 'color' => $faker->hexColor(), 'is_default' => 0));
            $Category->postAction(Action::Create, array('name' => 'Discussions', 'color' => $faker->hexColor(), 'is_default' => 0));
            $Category->postAction(Action::Create, array('name' => 'Production', 'color' => $faker->hexColor(), 'is_default' => 0));
            $Category->postAction(Action::Create, array('name' => 'R&D', 'color' => $faker->hexColor(), 'is_default' => 0));
            $Category->postAction(Action::Create, array('name' => 'Support ticket', 'color' => $faker->hexColor(), 'is_default' => 0));

            $Status->postAction(Action::Create, array('name' => 'Maintenance mode', 'color' => $faker->hexColor(), 'is_default' => 0));
            $Status->postAction(Action::Create, array('name' => 'Operational', 'color' => $faker->hexColor(), 'is_default' => 0));
            $Status->postAction(Action::Create, array('name' => 'In stock', 'color' => $faker->hexColor(), 'is_default' => 0));
            $Status->postAction(Action::Create, array('name' => 'Need to reorder', 'color' => $faker->hexColor(), 'is_default' => 0));
            $Status->postAction(Action::Create, array('name' => 'Destroyed', 'color' => $faker->hexColor(), 'is_default' => 0));
            $Status->postAction(Action::Create, array('name' => 'Processed', 'color' => $faker->hexColor(), 'is_default' => 0));
            $Status->postAction(Action::Create, array('name' => 'Waiting', 'color' => $faker->hexColor(), 'is_default' => 0));
            $Status->postAction(Action::Create, array('name' => 'Open', 'color' => $faker->hexColor(), 'is_default' => 0));
            $Status->postAction(Action::Create, array('name' => 'Closed', 'color' => $faker->hexColor(), 'is_default' => 0));

            $columns = array(
                'visible',
                'onboarding_email_body',
                'onboarding_email_subject',
                'onboarding_email_active',
            );
            foreach ($columns as $column) {
                if (isset($team[$column])) {
                    $Teams->patch(Action::Update, array($column => (string) $team[$column]));
                }
            }
        }

        $iterations = $yaml['iterations'] ?? self::DEFAULT_ITERATIONS;
        $Populate = new Populate((int) $iterations);

        // create users
        // all users have the same password to make switching accounts easier
        // if the password is provided in the config file, it'll be used instead for that user
        foreach ($yaml['users'] as $user) {
            $Populate->createUser($Teams, $user);
        }

        // create predefined templates
        if (isset($yaml['templates'])) {
            foreach ($yaml['templates'] as $template) {
                $user = new Users((int) ($template['user'] ?? 1), (int) ($template['team'] ?? 1));
                $Templates = new Templates($user);
                $id = $Templates->postAction(Action::Create, array());
                $Templates->setId($id);
                $patch = array(
                    'title' => $template['title'],
                    'body' => $template['body'] ?? '',
                    'category' => $template['category'] ?? 2,
                    'status' => $template['status'] ?? 2,
                    'metadata' => $template['metadata'] ?? '{}',
                );
                $Templates->patch(Action::Update, $patch);
                if (isset($template['locked'])) {
                    $Templates->toggleLock();
                }
                if (isset($template['tags'])) {
                    foreach ($template['tags'] as $tag) {
                        $Templates->Tags->postAction(Action::Create, array('tag' => $tag));
                    }
                }
                if (isset($template['items_links'])) {
                    foreach ($template['items_links'] as $target) {
                        $Templates->ItemsLinks->setId($target);
                        $Templates->ItemsLinks->postAction(Action::Create, array());
                    }
                }
            }
        }

        if (isset($yaml['experiments'])) {
            // read defined experiments
            foreach ($yaml['experiments'] as $experiment) {
                $user = new Users((int) ($experiment['user'] ?? 1), (int) ($experiment['team'] ?? 1));
                $Experiments = new Experiments($user);
                $id = $Experiments->postAction(Action::Create, array());
                $Experiments->setId($id);
                $patch = array(
                    'title' => $experiment['title'],
                    'body' => $experiment['body'] ?? '',
                    'date' => $experiment['date'],
                    'category' => $experiment['category'] ?? 2,
                    'status' => $experiment['status'] ?? 2,
                    'metadata' => $experiment['metadata'] ?? '{}',
                    'rating' => $experiment['rating'] ?? 0,
                );
                $Experiments->patch(Action::Update, $patch);
                if (isset($experiment['locked'])) {
                    $Experiments->toggleLock();
                }
                if (isset($experiment['tags'])) {
                    foreach ($experiment['tags'] as $tag) {
                        $Experiments->Tags->postAction(Action::Create, array('tag' => $tag));
                    }
                }
                if (isset($experiment['comments'])) {
                    foreach ($experiment['comments'] as $comment) {
                        $Experiments->Comments->postAction(Action::Create, array('comment' => $comment));
                    }
                }
                if (isset($experiment['experiments_links'])) {
                    foreach ($experiment['experiments_links'] as $target) {
                        $Experiments->ExperimentsLinks->setId($target);
                        $Experiments->ExperimentsLinks->postAction(Action::Create, array());
                    }
                }
                if (isset($experiment['items_links'])) {
                    foreach ($experiment['items_links'] as $target) {
                        $Experiments->ItemsLinks->setId($target);
                        $Experiments->ItemsLinks->postAction(Action::Create, array());
                    }
                }
            }
        }

        // delete the default items_types
        // add more items types
        if (array_key_exists('items_types', $yaml)) {
            foreach ($yaml['items_types'] as $items_types) {
                $user = new Users(1, (int) ($items_types['team'] ?? 1));
                $ItemsTypes = new ItemsTypes($user);
                $ItemsTypes->setId($ItemsTypes->create($items_types['name']));
                $ItemsTypes->bypassWritePermission = true;
                $defaultPermissions = BasePermissions::Team->toJson();
                $patch = array(
                    'color' => $items_types['color'],
                    'body' => $items_types['template'] ?? '',
                    'canread' => $defaultPermissions,
                    'canwrite' => $defaultPermissions,
                    'metadata' => $items_types['metadata'] ?? '{}',
                );
                $ItemsTypes->patch(Action::Update, $patch);
            }
        }

        // randomize the entries so they look like they are not added at once
        if (isset($yaml['items'])) {
            shuffle($yaml['items']);
            foreach ($yaml['items'] as $item) {
                $user = new Users((int) ($item['user'] ?? 1), (int) ($item['team'] ?? 1));
                $Items = new Items($user);
                $id = $Items->postAction(Action::Create, array('category_id' => $item['category']));
                $Items->setId($id);
                $patch = array(
                    'title' => $item['title'],
                    'body' => $item['body'] ?? '',
                    'date' => $item['date'] ?? date('Ymd'),
                    'rating' => $item['rating'] ?? 0,
                );
                // don't override the items type metadata
                if (isset($item['metadata'])) {
                    $patch['metadata'] = $item['metadata'];
                }
                if (isset($item['experiments_links'])) {
                    foreach ($item['experiments_links'] as $target) {
                        $ExperimentsLinks = new ExperimentsLinks($Items, (int) $target);
                        $ExperimentsLinks->postAction(Action::Create, array());
                    }
                }
                if (isset($item['items_links'])) {
                    foreach ($item['items_links'] as $target) {
                        $ExperimentsLinks = new ItemsLinks($Items, (int) $target);
                        $ExperimentsLinks->postAction(Action::Create, array());
                    }
                }
                $Items->patch(Action::Update, $patch);
            }
        }

        $output->writeln('All done.');
        return Command::SUCCESS;
    }

    private function dropAndInitDb(): void
    {
        $Db = Db::getConnection();
        $Db->q('DROP database ' . Config::fromEnv('DB_NAME'));
        $Db->q('CREATE database ' . Config::fromEnv('DB_NAME'));
        $Db->q('USE ' . Config::fromEnv('DB_NAME'));

        // load structure
        $Sql = new Sql(new Fs(new LocalFilesystemAdapter(dirname(__DIR__) . '/sql')));
        $Sql->execFile('structure.sql');
    }
}