intraxia/wp-gistpen

View on GitHub
app/Listener/Database.php

Summary

Maintainability
B
5 hrs
Test Coverage
<?php
namespace Intraxia\Gistpen\Listener;

use Intraxia\Gistpen\Database\EntityManager as EM;
use Intraxia\Gistpen\Model\Blob;
use Intraxia\Gistpen\Model\Commit;
use Intraxia\Gistpen\Model\Repo;
use Intraxia\Gistpen\Model\State;
use Intraxia\Jaxion\Axolotl\Collection;
use Intraxia\Jaxion\Contract\Axolotl\EntityManager;
use Intraxia\Jaxion\Contract\Core\HasActions;

/**
 * Database listener service.
 */
class Database implements HasActions {

    /**
     * Database service.
     *
     * @var EntityManager
     */
    private $em;

    /**
     * Database constructor.
     *
     * @param EntityManager $em
     */
    public function __construct( EntityManager $em ) {
        $this->em = $em;
    }

    /**
     * Checks if the Repo has changed and creates a new commit if it has.
     *
     * @param Repo $repo
     */
    public function add_commit( Repo $repo ) {
        $commits = $this->em->find_by( Commit::class, array(
            'repo_id' => $repo->ID,
            'with'    => array(
                'states' => array(
                    'with' => 'language',
                ),
            ),
            'orderby' => 'date',
            'order'   => 'DESC',
        ) );

        if ( $commits->count() > 0 && $this->matches_last_commit( $repo, $commits->first() ) ) {
            return;
        }

        $new_commit = new Commit( array(
            'repo_id'     => $repo->ID,
            'description' => $repo->description,
            'author'      => get_current_user_id(),
        ) );

        // This is the first commit.
        if ( $commits->count() === 0 ) {
            $states = new Collection( State::class );

            foreach ( $repo->blobs as $blob ) {
                $states = $states->add( $this->blob_to_state( $blob ) );
            }

            $new_commit->set_attribute( 'states', $states );

            $this->em->persist( $new_commit );

            return;
        }

        $prev_commit = $commits->first();

        // Get all the blobs that don't have a matching state in the previous commit.
        // These blobs are new.
        $added_states = $repo->blobs->filter( function ( Blob $blob ) use ( $prev_commit ) {
            return ! $prev_commit->states->contains( function ( State $state ) use ( $blob ) {
                return $state->blob_id === $blob->ID;
            } );
        } )
            // Save the blob's state.
            ->map( function ( Blob $blob ) {
                return $this->em->persist( $this->blob_to_state( $blob ) );
            } );

        // Remove all the states that don't match an existing blob.
        $states = $prev_commit->states->filter( function ( State $state ) use ( $repo ) {
            return $repo->blobs->contains( function ( Blob $blob ) use ( $state ) {
                return $state->blob_id === $blob->ID;
            } );
        } )
            ->map( function ( State $state ) use ( $repo ) {
                $blob = $repo->blobs->find( function ( Blob $blob ) use ( $state ) {
                    return $state->blob_id === $blob->ID;
                } );

                switch ( true ) {
                    // Create a new state for blobs that have changed.
                    case $blob->filename !== $state->filename:
                    case $blob->code !== $state->code:
                    case $blob->language->slug !== $state->language->slug:
                        return $this->em->persist( $this->blob_to_state( $blob ) );
                    // Otherwise, keep it.
                    default:
                        return $state;
                }
            } )->merge( $added_states );

        $new_commit->state_ids = $states->map( function ( State $state ) {
            return $state->ID;
        } )->to_array();

        $this->em->persist( $new_commit );
    }

    /**
     * Remove the action hook to save a post revision
     *
     * We're going to be handling this ourselves
     *
     * @param  int $post_id
     *
     * @since  0.5.0
     */
    public function remove_revision_save( $post_id ) {
        if ( 'gistpen' === get_post_type( $post_id ) ) {
            remove_action( 'post_updated', 'wp_save_post_revision', 10 );
        }
    }

    /**
     * Deletes the related Blobs when a Repo gets deleted.
     *
     * Is this something that should be absorbed by Jaxion\Axolotl?
     *
     * @param  int $post_id post ID of the zip being deleted.
     *
     * @since  0.5.0
     */
    public function delete_blobs( $post_id ) {
        $post = get_post( $post_id );

        if ( 'gistpen' === $post->post_type && 0 === $post->post_parent ) {
            $blobs = $this->em->find_by( \Intraxia\Gistpen\Model\Blob::class, array(
                'post_parent' => $post_id,
                'post_status' => 'any',
                'order'       => 'ASC',
                'orderby'     => 'date',
            ) );

            /* Blob $blob */
            foreach ( $blobs as $blob ) {
                wp_delete_post( $blob->ID, true );
            }
        }
    }

    /**
     * Allows empty Repo to save.
     *
     * @param  bool  $maybe_empty Whether post should be considered empty.
     * @param  array $postarr Array of post data.
     *
     * @return bool                Result of empty check
     * @since  0.5.0
     */
    public function allow_empty_zip( $maybe_empty, $postarr ) {
        if ( 'gistpen' === $postarr['post_type'] && 0 === $postarr['post_parent'] ) {
            $maybe_empty = false;
        }

        return $maybe_empty;
    }

    /**
     * Disables checking for changes when we save a post revision
     *
     * @param  bool     $check_for_changes whether we check for changes.
     * @param  \WP_Post $last_revision previous revision object.
     * @param  \WP_Post $post current revision.
     *
     * @return bool                        Whether we check for changes.
     * @since  0.5.0
     */
    public function disable_check_for_change( $check_for_changes, $last_revision, $post ) {
        if ( 'gistpen' === $post->post_type && 0 === $post->post_parent ) {
            $check_for_changes = false;
        }

        return $check_for_changes;
    }

    /**
     * Provides the array of actions the class wants to register with WordPress.
     *
     * These actions are retrieved by the Loader class and used to register the
     * correct service methods with WordPress.
     *
     * @return array[]
     */
    public function action_hooks() {
        return array(
            array(
                'hook'   => 'wpgp.create.repo',
                'method' => 'add_commit',
            ),
            array(
                'hook'   => 'wpgp.persist.repo',
                'method' => 'add_commit',
            ),
            array(
                'hook'     => 'post_updated',
                'method'   => 'remove_revision_save',
                'priority' => 9,
            ),
            array(
                'hook'   => 'before_delete_post',
                'method' => 'delete_blobs',
            ),
        );
    }

    /**
     * {@inheritDoc}
     *
     * @return array[]
     */
    public function filter_hooks() {
        return array(
            array(
                'hook'   => 'wp_insert_post_empty_content',
                'method' => 'allow_empty_zip',
                'args'   => 2,
            ),
            array(
                'hook'   => 'wp_save_post_revision_check_for_changes',
                'method' => 'disable_check_for_change',
                'args'   => 3,
            ),
        );
    }

    /**
     * Matches up the commit and repo and determines whether it has changed.
     *
     * @param Repo   $repo
     * @param Commit $commit
     *
     * @return bool
     */
    private function matches_last_commit( Repo $repo, Commit $commit ) {
        if ( $repo->description !== $commit->description ) {
            return false;
        }

        if ( $repo->blobs->count() !== $commit->states->count() ) {
            return false;
        }

        $has_changed_blobs = $repo->blobs->filter( function ( Blob $blob ) use ( $commit ) {
                return ! $commit->states->contains( function ( State $state ) use ( $blob ) {
                    if ( $state->blob_id === $blob->ID &&
                        $blob->filename === $state->filename &&
                        $blob->code === $state->code &&
                        $blob->language->slug === $state->language->slug
                    ) {
                        return true;
                    }

                    return false;
                } );
        } )->count() > 0;

        if ( $has_changed_blobs ) {
            return false;
        }

        return true;
    }

    /**
     * Map the Blob to a matching state.
     *
     * @param Blob $blob
     *
     * @return State
     */
    private function blob_to_state( Blob $blob ) {
        return new State( array(
            'blob_id'  => $blob->ID,
            'filename' => $blob->filename,
            'code'     => $blob->code,
            'language' => $blob->language,
        ) );
    }
}