REBELinBLUE/deployer

View on GitHub
app/Project.php

Summary

Maintainability
B
4 hrs
Test Coverage
<?php

namespace REBELinBLUE\Deployer;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use McCool\LaravelAutoPresenter\HasPresenter;
use REBELinBLUE\Deployer\Traits\BroadcastChanges;
use REBELinBLUE\Deployer\Traits\ProjectRelations;
use REBELinBLUE\Deployer\View\Presenters\ProjectPresenter;
use UnexpectedValueException;
use Version\Compare as VersionCompare;

/**
 * Project model.
 */
class Project extends Model implements HasPresenter
{
    use SoftDeletes, BroadcastChanges, ProjectRelations;

    const FINISHED     = 0;
    const PENDING      = 1;
    const DEPLOYING    = 2;
    const FAILED       = 3;
    const NOT_DEPLOYED = 4;

    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = ['name', 'repository', 'branch', 'group_id', 'include_dev',
                           'builds_to_keep', 'url', 'build_url', 'allow_other_branch',
                           'private_key', ];

    /**
     * The attributes excluded from the model's JSON form.
     *
     * @var array
     */
    protected $hidden = ['private_key', 'created_at', 'deleted_at', 'updated_at', 'hash',
                         'updated_at', 'servers', 'commands', 'hash',
                         'group', 'servers', 'commands', 'heartbeats', 'checkUrls',
                         'notifications', 'deployments', 'shareFiles', 'configFiles', 'is_mirroring', ];

    /**
     * Additional attributes to include in the JSON representation.
     *
     * @var array
     */
    protected $appends = ['group_name', 'webhook_url', 'repository_path', 'repository_url',
                          'branch_url', 'tags', 'branches', ];

    /**
     * The attributes that should be casted to native types.
     *
     * @var array
     */
    protected $casts = [
        'id'                 => 'integer',
        'group_id'           => 'integer',
        'status'             => 'integer',
        'builds_to_keep'     => 'integer',
        'allow_other_branch' => 'boolean',
        'include_dev'        => 'boolean',
        'is_mirroring'       => 'boolean',
    ];

    /**
     * The fields which should be treated as Carbon instances.
     *
     * @var array
     */
    protected $dates = ['last_run', 'last_mirrored'];

    /**
     * The heart beats status count.
     *
     * @var array
     */
    protected $heartbeatStatus = [];

    /**
     * The check url's status count.
     *
     * @var array
     */
    protected $checkurlStatus = [];

    /**
     * Determines whether the project is currently being deployed.
     *
     * @return bool
     */
    public function isDeploying()
    {
        return ($this->status === self::DEPLOYING || $this->status === self::PENDING);
    }

    /**
     * Generates a hash for use in the webhook URL.
     */
    public function generateHash()
    {
        $this->attributes['hash'] = token(60);
    }

    /**
     * Parses the repository URL to get the user, domain, port and path parts.
     *
     * @return array
     */
    public function accessDetails()
    {
        $info = [];

        if (preg_match('#^(.+)://(.+)@(.+):([0-9]*)\/?(.+)\.git$#', $this->repository, $matches)) {
            $info['scheme']    = strtolower($matches[1]);
            $info['user']      = $matches[2];
            $info['domain']    = $matches[3];
            $info['port']      = $matches[4];
            $info['reference'] = $matches[5];
        } elseif (preg_match('#^(.+)@(.+):([0-9]*)\/?(.+)\.git$#', $this->repository, $matches)) {
            $info['scheme']    = 'git';
            $info['user']      = $matches[1];
            $info['domain']    = $matches[2];
            $info['port']      = $matches[3];
            $info['reference'] = $matches[4];
        } elseif (preg_match('#^https?://#i', $this->repository)) {
            $data = parse_url($this->repository);

            if (!$data) {
                return $info;
            }

            $info['scheme']    = strtolower($data['scheme']);
            $info['user']      = isset($data['user']) ? $data['user'] : '';
            $info['domain']    = $data['host'];
            $info['port']      = isset($data['port']) ? $data['port'] : '';
            $info['reference'] = substr($data['path'], 1, -4);
        }

        return $info;
    }

    /**
     * Gets the repository path.
     *
     * @return string|false
     *
     * @see Project::accessDetails()
     */
    public function getRepositoryPathAttribute()
    {
        $info = $this->accessDetails();

        if (isset($info['reference'])) {
            return $info['reference'];
        }

        return false;
    }

    /**
     * Gets the HTTP URL to the repository.
     *
     * @return string|false
     *
     * @see Project::accessDetails()
     */
    public function getRepositoryUrlAttribute()
    {
        $info = $this->accessDetails();

        if (isset($info['domain']) && isset($info['reference'])) {
            if (!isset($info['scheme']) || !starts_with($info['scheme'], 'http')) {
                $info['scheme'] = 'http';
            }

            // Always serve github links over HTTPS
            if (ends_with($info['domain'], 'github.com')) {
                $info['scheme'] = 'https';
            }

            return $info['scheme'] . '://' . $info['domain'] . '/' . $info['reference'];
        }

        return false;
    }

    /**
     * Gets the view presenter.
     *
     * @return string
     */
    public function getPresenterClass()
    {
        return ProjectPresenter::class;
    }

    /**
     * Gets the HTTP URL to the branch.
     *
     * @param string $alternative
     *
     * @return string|false
     *
     * @see Project::accessDetails()
     */
    public function getBranchUrlAttribute($alternative = null)
    {
        $info = $this->accessDetails();

        if (isset($info['domain']) && isset($info['reference'])) {
            $path = 'tree';
            if (str_contains($info['domain'], 'bitbucket')) {
                $path = 'commits/branch';
            }

            if (!isset($info['scheme']) || !starts_with($info['scheme'], 'http')) {
                $info['scheme'] = 'http';
            }

            // Always serve github links over HTTPS
            if (ends_with($info['domain'], 'github.com')) {
                $info['scheme'] = 'https';
            }

            $branch = (is_null($alternative) ? $this->branch : $alternative);

            return $info['scheme'] . '://' . $info['domain'] . '/' . $info['reference'] . '/' . $path . '/' . $branch;
        }

        return false;
    }

    /**
     * Count the missed heartbeat.
     *
     * @return array
     */
    public function heartbeatsStatus()
    {
        if (empty($this->heartbeatStatus)) {
            $length = count($this->heartbeats);
            $missed = 0;

            foreach ($this->heartbeats as $beat) {
                if (!$beat->isHealthy()) {
                    $missed++;
                }
            }

            $this->heartbeatStatus = ['missed' => $missed, 'length' => $length];
        }

        return $this->heartbeatStatus;
    }

    /**
     * Count the application url check status.
     *
     * @return array
     */
    public function applicationCheckUrlStatus()
    {
        if (empty($this->checkurlStatus)) {
            $length = count($this->checkUrls);
            $missed = 0;

            foreach ($this->checkUrls as $link) {
                if (!$link->isHealthy()) {
                    $missed++;
                }
            }

            $this->checkurlStatus = ['missed' => $missed, 'length' => $length];
        }

        return $this->checkurlStatus;
    }

    /**
     * Define a accessor for the group name.
     *
     * @return string
     */
    public function getGroupNameAttribute()
    {
        return $this->group->name;
    }

    /**
     * Define an accessor for the webhook URL.
     *
     * @return string
     */
    public function getWebhookUrlAttribute()
    {
        return route('webhook.deploy', $this->hash);
    }

    /**
     * Gets the list of all tags for the project.
     *
     * @return \Illuminate\Support\Collection
     */
    public function getTagsAttribute()
    {
        $tags = $this->refs()
                     ->where('is_tag', true)
                     ->pluck('name')
                     ->toArray();

        $compare = new VersionCompare();

        // Sort the tags, if compare throws an exception it isn't a value version string so just do a strnatcmp
        // See #258 - Can remove the @ when dropping PHP 5 support
        @usort($tags, function ($first, $second) use ($compare) {
            try {
                return $compare->compare($first, $second);
            } catch (UnexpectedValueException $error) {
                return strnatcmp($second, $first); // Move unknown versions to be bottle, swap round to move to top
            }
        });

        return collect($tags)->reverse()->values();
    }

    /**
     * Gets the list of all branches for the project which are not the default.
     *
     * @return array
     */
    public function getBranchesAttribute()
    {
        return $this->refs()
                    ->where('is_tag', false)
                    ->where('name', '<>', $this->branch)
                    ->orderBy('name')
                    ->pluck('name');
    }

    /**
     * Generate a friendly path for the mirror of the repository.
     * Use the repository rather than the project ID, so if a single
     * repo is used in multiple projects it is not duplicated.
     *
     * @return string
     */
    public function mirrorPath()
    {
        return storage_path('app/mirrors/' . preg_replace('/[^_\-.\-a-zA-Z0-9\s]/u', '_', $this->repository));
    }

    /**
     * Belongs to relationship.
     *
     * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
     */
    public function group()
    {
        return $this->belongsTo(Group::class);
    }

    /**
     * Has many relationship.
     *
     * @return \Illuminate\Database\Eloquent\Relations\HasMany
     */
    public function servers()
    {
        return $this->hasMany(Server::class)
                    ->orderBy('order', 'ASC');
    }

    /**
     * Has many relationship.
     *
     * @return \Illuminate\Database\Eloquent\Relations\HasMany
     */
    public function heartbeats()
    {
        return $this->hasMany(Heartbeat::class)
                    ->orderBy('name');
    }

    /**
     * Has many relationship.
     *
     * @return \Illuminate\Database\Eloquent\Relations\HasMany
     */
    public function deployments()
    {
        return $this->hasMany(Deployment::class)
                    ->orderBy('started_at', 'DESC');
    }

    /**
     * Has many relationship.
     *
     * @return \Illuminate\Database\Eloquent\Relations\HasMany
     */
    public function channels()
    {
        return $this->hasMany(Channel::class)
                    ->orderBy('name');
    }

    /**
     * Has many urls to check.
     *
     * @return \Illuminate\Database\Eloquent\Relations\HasMany
     */
    public function checkUrls()
    {
        return $this->hasMany(CheckUrl::class);
    }

    /**
     * Has many relationship for git references.
     *
     * @return \Illuminate\Database\Eloquent\Relations\HasMany
     *
     * @see Project::getTagsAttribute()
     * @see Project::getBranchesAttribute()
     */
    public function refs()
    {
        return $this->hasMany(Ref::class);
    }

    /**
     * Has many relationship for users.
     *
     * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
     */
    public function users()
    {
        return $this->belongsToMany(User::class)->withPivot('role');
    }
}