Having attachments in database with wp-browser

WPDb module now supporting attachments explicitly.

A fundamental post type

While WordPress installations can undergo a large amount of modifications aimed at adding and removing post types, functionalities and supports, usually a post type surviving modifications is the attachment one.
The reason being that its inherent neutrality, it represents a generic attachment after all, and usefulness makes it one of the fundamental moving pieces in a CMS.
The WPDb module, part of wp-browser, has explicitly supported the post and page post types so far but the time had come to support attachments too.
I could manage to test for and with attachments in acceptance and functional tests (or any level of testing where WordPress code was not loaded in the testing scope) using some wizardry, but it was not that easy.
the addition, some time ago, of the loadOnly option to the WPLoader module then has more than covered the issue so far.
Still: not that explicit and so easy.

An example usage

There are a number of things the WPDb module new method, haveAttachmentInDatabase, does under the hood; for all intents and purposes the method emulates what WordPress does when uploading an image.
In a default WordPress installation those operations are:

  • moving the original uploaded file in the uploads folder
  • creating an image version for each size specified by the theme or plugins currently active if the attachment is an image
  • create a bunch of entries in the database to keep track of the new attachment and its meta values

Keeping in mind the WPDb module cannot, and should not, access WordPress code to do its job some additional work is required from the testers to make it behave exactly as intended.
Since the haveAttachmentInDatabase will move files around it requires the WPFilesystem module to work; an example suite configuration could look like this (I’m using Codeception dynamic parameter configuration in the example):

class_name: FunctionalTester
modules:
    enabled:
      - FunctionalHelper
      - WPFilesystem
      - WPDb
      - Asserts
    config:
      WPFilesystem:
        wpRootFolder: %WP_ROOT_FOLDER%
      WPDb:
        dsn: 'mysql:host=%DB_HOST%;dbname=%DB_NAME%'
        user: %DB_USER%
        password: %DB_PASSWORD%
        dump: 'tests/_data/dump.sql'
        populate: true
        cleanup: true
        reconnect: false
        url: '%WP_URL%'
        tablePrefix: 'wp_'

Once this requirement is met I can use the method in its most basic incarnation and let some reasonable defaults kick in:

public function test_basic_use(FunctionalTester $I) {
    $file = codecept_data_dir('attachments/kitten.jpeg');

    $id   = $I->haveAttachmentInDatabase($file);

    // ...
}

Provided just a file path the module will create a copy of the file in the uploads folder, create a thumbnail, medium and large version of it, and update the attachment metadata.
Here is the code that verifies the above behaviour in a test:

public function test_basic_use(FunctionalTester $I) {
    $file = codecept_data_dir('attachments/kitten.jpeg');

    $id   = $I->haveAttachmentInDatabase($file);

    $year  = date('Y');
    $month = date('m');

    $criteria = [
        'post_type'      => 'attachment',
        'post_title'     => 'kitten',
        'post_status'    => 'inherit',
        'post_name'      => 'kitten',
        'post_parent'    => '0',
        'guid'           => $I->grabSiteUrl("/wp-content/uploads/{$year}/{$month}/kitten.jpeg"),
        'post_mime_type' => 'image/jpeg',
    ];

    foreach ($criteria as $key => $value) {
        $I->seePostInDatabase(['ID' => $id, $key => $value]);
    }

    $I->seeUploadedFileFound('kitten.jpeg', 'now');
    $I->seeUploadedFileFound('kitten-150x150.jpeg', 'now');
    $I->seeUploadedFileFound('kitten-300x200.jpeg', 'now');
    $I->seeUploadedFileFound('kitten-768x512.jpeg', 'now');

    $I->seePostMetaInDatabase(['post_id' => $id, 'meta_key' => '_wp_attached_file', 'meta_value' => "{$year}/{$month}/kitten.jpeg"]);
    $metadata = [
        'width'      => 1000,
        'height'     => 667,
        'file'       => "{$year}/{$month}/kitten.jpeg",
        'sizes'      => [
            'thumbnail' => [
                'file'      => 'kitten-150x150.jpeg',
                'width'     => 150,
                'height'    => 150,
                'mime-type' => 'image/jpeg',
            ],
            'medium'    => [
                'file'      => 'kitten-300x200.jpeg',
                'width'     => 300,
                'height'    => 200,
                'mime-type' => 'image/jpeg',
            ],
            'large'     => [
                'file'      => 'kitten-768x512.jpeg',
                'width'     => 768,
                'height'    => 512,
                'mime-type' => 'image/jpeg',
            ],
        ],
        'image_meta' =>
            [
                'aperture'          => '0',
                'credit'            => '',
                'camera'            => '',
                'caption'           => '',
                'created_timestamp' => '0',
                'copyright'         => '',
                'focal_length'      => '0',
                'iso'               => '0',
                'shutter_speed'     => '0',
                'title'             => '',
                'orientation'       => '0',
                'keywords'          => [],
            ],
    ];
    $I->seePostMetaInDatabase(['post_id' => $id, 'meta_key' => '_wp_attachment_metadata', 'meta_value' => serialize($metadata)]);
}

If the attachment is not an image then no additional versions of it will be generated and the method will only create the _wp_attached_file meta entry:

public function test_non_image_attachments(FunctionalTester $I) {
    $file = codecept_data_dir('attachments/pdf-doc.pdf');

    $id = $I->haveAttachmentInDatabase($file);

    $year  = date('Y');
    $month = date('m');

    $criteria = [
        'post_type'      => 'attachment',
        'post_title'     => 'pdf-doc',
        'post_status'    => 'inherit',
        'post_name'      => 'pdf-doc',
        'post_parent'    => '0',
        'guid'           => $I->grabSiteUrl("/wp-content/uploads/{$year}/{$month}/pdf-doc.pdf"),
        'post_mime_type' => 'application/pdf',
    ];

    foreach ($criteria as $key => $value) {
        $I->seePostInDatabase(['ID' => $id, $key => $value]);
    }

    $I->seeUploadedFileFound('pdf-doc.pdf', 'now');
    $I->seePostMetaInDatabase(['post_id' => $id, 'meta_key' => '_wp_attached_file', 'meta_value' => "{$year}/${month}/pdf-doc.pdf"]);
}

Moar power

The method behaviour can be controlled with additional arguments to fine tune the attachment creation; tests can specify:

  • a date argument to set the uploads year/month folder
  • an overrides argument to control the generated post fields
  • an imageSizes array to control what alternative sizes, if any, will be generated when adding the attachment
public function should_allow_definining_the_image_sizes_to_create(FunctionalTester $I) {
    $file       = codecept_data_dir('attachments/kitten.jpeg');
    $date       = '2016-01-01';
    $imageSizes = [
        'thumbnail' => [200, 200],
        'normal'    => 500,
        'foo'       => [450, 130],
    ];

    $id = $I->haveAttachmentInDatabase($file, $date, ['post_title' => 'foo'], $imageSizes);

    $I->seeUploadedFileFound('kitten.jpeg', $date);
    $I->seeUploadedFileFound('kitten-200x200.jpeg', $date);
    $I->seeUploadedFileFound('kitten-500x333.jpeg', $date);
    $I->seeUploadedFileFound('kitten-450x130.jpeg', $date);

    $I->seePostMetaInDatabase(['post_id' => $id, 'meta_key' => '_wp_attached_file', 'meta_value' => "2016/01/kitten.jpeg"]);
    $metadata = [
        'width'      => 1000,
        'height'     => 667,
        'file'       => "2016/01/kitten.jpeg",
        'sizes'      => [
            'thumbnail' => [
                'file'      => 'kitten-200x200.jpeg',
                'width'     => 200,
                'height'    => 200,
                'mime-type' => 'image/jpeg',
            ],
            'normal'    => [
                'file'      => 'kitten-500x333.jpeg',
                'width'     => 500,
                'height'    => 333,
                'mime-type' => 'image/jpeg',
            ],
            'foo'     => [
                'file'      => 'kitten-450x130.jpeg',
                'width'     => 450,
                'height'    => 130,
                'mime-type' => 'image/jpeg',
            ],
        ],
        'image_meta' =>
            [
                'aperture'          => '0',
                'credit'            => '',
                'camera'            => '',
                'caption'           => '',
                'created_timestamp' => '0',
                'copyright'         => '',
                'focal_length'      => '0',
                'iso'               => '0',
                'shutter_speed'     => '0',
                'title'             => '',
                'orientation'       => '0',
                'keywords'          => [],
            ],
    ];
    $I->seePostMetaInDatabase(['post_id' => $id, 'meta_key' => '_wp_attachment_metadata', 'meta_value' => serialize($metadata)]);
}

The method comes with a bunch of utility others:

  • dontHaveAttachmentInDatabase to remove an attachment database data, use WPFilesystem module to remove the files
  • seeAttachmentInDatabase to check that an attachment exists on a database level
  • dontSeeAttachmentInDatabase to check that an attachment doesn’t exist on a database level

Attachment methods deal, in general, with the database side of things. Should I need to have database and file system management rolled into a method creating an ad-hoc module for the project is quite easy.