Commit 4158dae1 authored by Marwen Amri's avatar Marwen Amri Committed by Łukasz Zaroda
Browse files

Issue #3257306 by Mario_o, Luke_Nuke: Add a detailed module documentation &...

Issue #3257306 by Mario_o, Luke_Nuke: Add a detailed module documentation & fix and add more examples to the content_fixtures_test module
parent 55dfc8a8
Loading
Loading
Loading
Loading
+218 −23
Original line number Diff line number Diff line
# CONTENTS OF THIS FILE

 * Introduction
 * How to
 * Requirements
 * Maintainers
# Content fixtures module

* [Introduction](#introduction)
* [Similar project](#similar-project)
* [Usage](#usage)
  * [Creating and loading fixtures](#creating-and-loading-fixtures)
    * [I.Creating a fixture](#icreating-a-fixture)
    * [II.Implementing the load method](#iiimplementing-the-load-method)
  * [Splitting fixtures into separate files](#splitting-fixtures-into-separate-files)
    * [I-Sharing objects between fixtures](#i-sharing-objects-between-fixtures)
    * [II-Loading the fixture files in specific order](#ii-loading-the-fixture-files-in-a-specific-order)
  * [Using fixture groups to only execute some fixtures](#fixture-groups-only-executing-some-fixtures)
  * [Execution](#execution)
* [Requirements](#requirements)
* [Maintainers](#maintainers)

## Introduction

@@ -21,43 +29,229 @@ ambigious states, that's why it deletes all content before loading any fixtures

Great match with Docker, if you are looking for ultimate automation.

## How to
## Similar project

This module has API very similar to [DoctrineFixturesBundle](https://symfony.com/doc/master/bundles/DoctrineFixturesBundle/index.html),
with some Drupal-specific differences/simplifications.

### Basics
## Usage

### Creating and loading fixtures

Minimally, your fixture class has to implement `FixtureInterface`, and be registered as a service tagged with a `content_fixture`
tag.

### Sharing objects between fixtures.
#### I.Creating a fixture

1. Inside your module src create **Fixture** directory (this is just a convention for storing fixtures).
2. Inside that folder create a class, ex: `ArticleFixture.php` that implements the `FixtureInterface` :
```php
 // see full code example at: /content_fixtures/modules/content_fixtures_example/src/Fixture/ArticleFixture.php
use Drupal\content_fixtures\Fixture\FixtureInterface;

class ArticleFixture implements FixtureInterface {
   /**
    * {@inheritDoc}
    */
   public function load() {

   }
}
```
3. Register the created class as a service and give it a tag name = `content_fixture`
```php
  services:
    content_fixtures_example.fixture.article:
      class: Drupal\content_fixtures_example\Fixture\ArticleFixture
      tags:
        - { name: content_fixture }
```
4. Clear the cache then execute `drush content-fixtures:list` to list all the created fixtures. The `ArticleFixture` should be on the list,
   it means that the Fixture has been created successfully.

#### II.Implementing the load method

1. Inject the `entity_type.manager` service into our Fixture class :
```php
   content_fixtures_example.article_fixture:
     class: Drupal\content_fixtures_example\Fixture\ArticleFixture
     arguments: [ '@entity_type.manager' ]
     tags:
       - { name: content_fixture }
```
2. The load method will use the injected entity_type.manager service to create a set of articles :
```php
    /**
   * The entity type manager service.
   *
   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
   */
  private $entityTypeManager;

  /**
   * Constructor.
   *
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
   *   The entity type manager.
   */
  public function __construct(EntityTypeManagerInterface $entityTypeManager) {
    $this->entityTypeManager = $entityTypeManager;
  }

 /**
   * {@inheritDoc}
   */
  public function load(): void {
    // Create nodes and save them into the DB.
    $nodeStorage = $this->entityTypeManager->getStorage('node');
    for ($i = 0; $i < 5; $i++) {
      $node = $nodeStorage->create([
        'type' => 'article',
        'title' => 'Article title - Group tutorial' . $i,
        'body' => 'Article title - Group tutorial' . $i,
      ]);
      $node->save();
    }
  }
```
3.Execute the `drush content-fixtures:load` command to execute the `load()` method and create the Articles.
> :information_source: When we execute `drush content-fixtures:load` all the `load` methods of all services tagged with `content_fixture` will be executed.

> :warning: The load command will remove all content from the site before inserting the new content, but don't worry, the command will ask you if you want to proceed or not.


###  Splitting fixtures into separate files
If we have a simple fixture, it will be fine to put it in just one class, but if we have multiple fixtures and class ends up being too long, hard to debug and extend in the future,
then it will be better to separate these fixtures across multiple classes.

#### I-Sharing objects between fixtures.
We can create an object using one fixture, then pass it to the next fixtures. This is especially useful if we want to set up references between objects created in multiple fixtures.

The easiest way of achieving this is to extend `AbstractFixture` class, that will provide you with some additional
sugar - this abstract class implements `SharedFixtureInterface` that will give you a way of sharing created objects
between fixtures.
The easiest way of achieving this is to extend `AbstractFixture` class that implements `SharedFixtureInterface`, which will provide you with some additional
methods that make it possible to share objects across fixtures:
- `addReference()` : exposes the object to the fixtures that will load after the one using this method.
- `getReference()` : allows you to access any object that has been exposed by the `addReference` method.

### Order of execution of fixtures
For example:

For this to work, you have to be able to decide on the order of execution of fixtures, and you can do
this in two ways:
1. Create a fixture class that extends the `AbstractFixture` class, this will allow you to use the `addReference` method later on.
  ```php
    // see full code example at: /content_fixtures/modules/content_fixtures_example/src/Fixture/splitting_fixtures/UserFixture.php
    use Drupal\content_fixtures\Fixture\AbstractFixture;

    class UserFixture extends AbstractFixture {}
  ```
2. Use the `addReference()` method to store the created $user object under the reference name `user`. It will make it accessible to the next fixtures, by using that reference name.
```php
    // see full code example at: /content_fixtures/modules/content_fixtures_example/src/Fixture/splitting_fixtures/UserFixture.php
    use Drupal\content_fixtures\Fixture\AbstractFixture;

    class UserFixture extends AbstractFixture {
        public const USER_REFERENCE = 'user';

        $this->addReference(self::USER_REFERENCE, $user);
    }
  ```

3. The object we just shared, will now be accessible for any other fixture that will run later, by using the `getReference()` method. We just need to address it by its reference name.
```php
    // see full code example at: /content_fixtures/modules/content_fixtures_example/src/Fixture/splitting_fixtures/ArticleDependentOnUserFixture.php
    use Drupal\content_fixtures\Fixture\AbstractFixture;

    class ArticleDependentOnUserFixture extends AbstractFixture {
      public function load(): void {
         /** @var \Drupal\user\Entity\User $user */
         $user = $this->getReference(UserFixture::USER_REFERENCE);

         $nodeStorage = $this->entityTypeManager->getStorage('node');
         $node = $nodeStorage->create([
          'type' => 'page',
          'title' => 'Fixture title - Splitting fixture tutorial',
           // This is another nice thing about the AbstractFixture class: it gives
           // us a random string generator.
           'body' => $this->getRandom()->paragraphs(2),
           'uid' => $user->id(),
         ]);
         $node->save();
      }
    }
  ```
#### II-Loading the fixture files in a specific order
The problem with the previous step that what if the NodeFixture has been loaded before the UserFixture
this will cause an error because the NodeFixture depends on the UserFixture (`'uid' => $user->id()`) so we need to
make sure that the `drush content-fixtures:load` will load the UserFixture first.

Fortunately, we can control the fixtures' execution order, by using one of these two methods (method 2 is recommended):

1. By implementing `OrderedFixtureInterface` - this will allow you to assign a value to each fixture, that will be used
   for ordering.
   ```php
   use Drupal\content_fixtures\Fixture\OrderedFixtureInterface;

   class NodeFixture extends AbstractFixture implements OrderedFixtureInterface  {

    public function getOrder() {
     return 2;
    }

   }

   ```
2. By implementing `DependentFixtureInterface`, that will allow you to declare dependencies between fixtures, and order
   of execution will be calculated by using this information.
   ```php
   // see the full example at : src/web/modules/contrib/content_fixtures/modules/content_fixtures_example/src/Fixture/splitting_fixtures/ArticleDependentOnUserFixture.php
   use Drupal\content_fixtures\Fixture\DependentFixtureInterface;

### Groups
   class ArticleDependentOnUserFixture extends AbstractFixture implements DependentFixtureInterface {
     public function getDependencies(): array {
      return [
          UserFixture::class,
      ];
    }
   }
   ```
   > :information_source: To display the fixtures' execution order just use : `drush content-fixture:list`

You can also implement `FixtureGroupInterface` in your fixture, to assign it to some custom groups. It will allow you
### Fixture Groups: Only executing some fixtures
The `drush content-fixtures:load` will load all existing fixtures, but what if we want to load only a specific set of fixtures?
To do that you can implement `FixtureGroupInterface` in your fixture, to assign it to some custom groups. It will allow you
to run fixtures by groups they belong to. This way you can have different set of fixtures for presentation, different
for development etc.
1. Make your class implement the `FixtureGroupInterface`. This interface has a method `getGroup()` that should return an array of arbitrary strings,
   representing your groups, so just return names of groups you want your fixture to belong to.
  ```php
   // see the full example at : /content_fixtures/modules/content_fixtures_example/src/Fixture/groups/ArticleBelongingToGroupFixture.php
   class ArticleBelongingToGroupFixture implements FixtureInterface, FixtureGroupInterface {
       public function getGroups(): array {
          return [
            'node_group',
          ];
        }
   }
   ```

  ```php
   // see the full example at : /content_fixtures/modules/content_fixtures_example/src/Fixture/groups/PageBelongingToGroupFixture.php
   class PageBelongingToGroupFixture.php implements FixtureInterface, FixtureGroupInterface {
       public function getGroups(): array {
          return [
            'node_group',
          ];
        }
   }
   ```
2. In order to display all the fixtures that belong to the `node_group`, use : `drush content-fixtures:list --groups=node_group`
3. In order to load only the fixtures that belong to the `node_group`, use : `drush content-fixtures:load --groups=node_group`

### Execution

You need `drush` to run your fixtures. Module provides you with two commands:
* `content-fixtures:list`
* `content-fixtures:load`
You need `drush` to run your fixtures. This module provides you with three commands with some options:
* `drush content-fixtures:list`: list all fixtures (services tagged using the `content_fixture` tag).
* `drush content-fixtures:load`: Delete all content, then load all fixtures.
* `drush content-fixtures:load --groups=group1`: load all fixtures that belong to a specific group.
* `drush content-fixtures:load --groups=group1,group2,group3`: load all fixtures that belong to any of these groups.
* `drush content-fixtures:purge`: delete all existing content on website.

See: `drush help content-fixtures:list` and `drush help content-fixtures:load` .

@@ -65,9 +259,10 @@ Happy coding!

## Requirements

* PHP >= 5.6
* Drush 8 / 9
* PHP >= 7.1
* Drush 9 / 10

## Maintainers

* Łukasz Zaroda <luken@luken-tech.pl>
* Marwen Amri <marwen.amri@ekino.com>
+8 −0
Original line number Diff line number Diff line
# Content fixtures example module

This module showcases example usage of the content_fixtures module.

## Requirements

For example fixtures to work correctly, you need the default content types provided by the "standard" installation
profile of Drupal.
+10 −0
Original line number Diff line number Diff line
name: 'Content Fixtures Example'
type: module
core_version_requirement: ^8.7.7 || ^9
package: Development
description: Content fixtures examples
dependencies:
  - drupal:user
  - drupal:node
  - drupal:file
  - content_fixtures:content_fixtures
+46 −0
Original line number Diff line number Diff line
services:

  # Helpers

  content_fixtures_example.file_provider.image:
    class: Drupal\content_fixtures_example\FileProvider\ImageProvider
    arguments: ['@file_system', '@config.factory']

  # Fixtures

  content_fixtures_example.fixture.article:
    class: Drupal\content_fixtures_example\Fixture\ArticleFixture
    arguments: ['@entity_type.manager']
    tags:
      - { name: content_fixture }

  content_fixtures_example.fixture.article_with_generated_image:
    class: Drupal\content_fixtures_example\Fixture\generating_files\ArticleWithImageFixture
    arguments: ['@entity_type.manager', '@content_fixtures_example.file_provider.image']
    tags:
      - { name: content_fixture }

  content_fixtures_example.fixture.node_dependent_on_user:
    class: Drupal\content_fixtures_example\Fixture\splitting_fixtures\ArticleDependentOnUserFixture
    arguments: ['@entity_type.manager']
    tags:
      - { name: content_fixture }

  content_fixtures_example.fixture.user:
    class: Drupal\content_fixtures_example\Fixture\splitting_fixtures\UserFixture
    arguments: ['@entity_type.manager']
    tags:
      - { name: content_fixture }

  content_fixtures_example.fixture.article_belonging_to_group:
    class: Drupal\content_fixtures_example\Fixture\groups\ArticleBelongingToGroupFixture
    arguments: ['@entity_type.manager']
    tags:
      - { name: content_fixture }

  content_fixtures_example.fixture.page_belonging_to_group:
    class: Drupal\content_fixtures_example\Fixture\groups\PageBelongingToGroupFixture
    arguments: ['@entity_type.manager']
    tags:
      - { name: content_fixture }
+62 −0
Original line number Diff line number Diff line
<?php

namespace Drupal\content_fixtures_example\FileProvider;

use Drupal\Component\Utility\Random;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Config\ImmutableConfig;
use Drupal\Core\File\FileSystemInterface;
use Drupal\file\Entity\File;
use Drupal\file\FileInterface;

/**
 * This is an example class to showcase how you can generate images for your
 * fixtures, by using only core Drupal features.
 *
 * Class ImageProvider.
 */
class ImageProvider {

  /** @var Random */
  private $random;

  /** @var FileSystemInterface */
  private $fileSystem;

  /** @var ImmutableConfig */
  private $systemFileConfig;


  public function __construct(FileSystemInterface $fileSystem, ConfigFactoryInterface $configFactory) {
    $this->fileSystem = $fileSystem;
    $this->systemFileConfig = $configFactory->get('system.file');
  }

  public function createImageFile($directory, $filename, $extension, $size = '400x400'): FileInterface {
    $imageDirectoryUri = $this->systemFileConfig->get('default_scheme') . "://$directory";
    $this->fileSystem->prepareDirectory($imageDirectoryUri, $this->fileSystem::CREATE_DIRECTORY);
    $imageUri = "$imageDirectoryUri/$filename.$extension";
    $this->getRandom()->image($this->fileSystem->realpath($imageUri), $size, $size);

    $imageFile = File::create([
      'uri' => $imageUri,
    ]);

    $imageFile->save();

    return $imageFile;
  }

  /**
   * Returns the random data generator.
   *
   * @return Random
   */
  private function getRandom(): Random {
    if (!$this->random) {
      $this->random = new Random();
    }
    return $this->random;
  }

}
Loading