GreenCape/joomla-manifest

View on GitHub
src/GreenCape/Manifest/Manifest.php

Summary

Maintainability
D
2 days
Test Coverage
<?php
/**
 * GreenCape Joomla Extension Manifests
 *
 * MIT License
 *
 * Copyright (c) 2014-2015, Niels Braczek <nbraczek@bsds.de>. All rights reserved.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
 * documentation files (the "Software"), to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and
 * to permit persons to whom the Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all copies or substantial portions
 * of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO
 * THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
 * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 *
 * @package         GreenCape\Manifest
 * @author          Niels Braczek <nbraczek@bsds.de>
 * @copyright   (C) 2014-2015 GreenCape, Niels Braczek <nbraczek@bsds.de>
 * @license         http://opensource.org/licenses/MIT The MIT license (MIT)
 * @link            http://greencape.github.io
 * @since           File available since Release 0.1.0
 */

namespace GreenCape\Manifest;

use BadMethodCallException;
use GreenCape\Xml\Converter;
use InvalidArgumentException;
use UnexpectedValueException;

/**
 * Class Manifest
 *
 * @package GreenCape\Manifest
 * @author  Niels Braczek <nbraczek@bsds.de>
 * @since   Class available since Release 0.1.0
 */
abstract class Manifest implements Section
{
    /**
     * Manifest Attributes
     */

    /**
     * @var string This attribute describes the type of the extension for the installer.
     *             Based on this type further requirements to sub-tags apply.
     */
    protected $type;

    /**
     * @var string String that identifies the version of Joomla for which this extension is developed.
     */
    protected $target = '2.5';

    /**
     * @var string Installation method, one of 'install', 'upgrade'
     *             The 'install' value means the installer will gracefully stop
     *             if it finds any existing file/folder of the new extension.
     */
    protected $method = 'install';

    /**
     * Metadata
     */

    /** @var string Raw component name (e.g. com_banners). This is a translatable field. */
    protected $name = '';

    /** @var string Author's name (e.g. Joomla! Project) */
    protected $author = '';

    /** @var string Date of creation or release (e.g. April 2006) */
    protected $creationDate = '';

    /** @var string The year (or range) for the copyright statement (e.g. 2005 - 2011) */
    protected $copyrightYear = '';

    /** @var string The owner of the copyright (e.g. Open Source Matters) */
    protected $copyrightOwner = '';

    /** @var string A license statement */
    protected $license = 'GNU General Public License version 2 or later; see LICENSE.txt';

    /** @var string Author's email address (e.g. admin@joomla.org) */
    protected $authorEmail = '';

    /** @var string URL to the author's website (e.g. www.joomla.org) */
    protected $authorUrl = '';

    /** @var string The version number of the extension (e.g. 1.6.0) */
    protected $version = '';

    /** @var string The description of the extension. This is a translatable field. (e.g. COM_BANNERS_XML_DESCRIPTION) */
    protected $description = '';

    /**
     * Sections
     */

    /** @var Section[] */
    protected $sections = [];

    /** @var array Section map */
    protected $map = [
        'install'       => 'SqlSection',
        'uninstall'     => 'SqlSection',
        'update'        => 'SchemaSection',
        'files'         => 'FileSection',
        'languages'     => 'LanguageSection',
        'media'         => 'MediaSection',
        'updateservers' => 'ServerSection',
        'params'        => 'VerbatimSection',
        'config'        => 'VerbatimSection',
    ];

    /**
     * Install hooks
     */

    /** @var string Install file, deprecated in 1.6 */
    protected $installFile;

    /** @var string Uninstall file, deprecated in 1.6 */
    protected $uninstallFile;

    /** @var string Install/update/uninstall file, new in 1.6 */
    protected $scriptFile;

    /**
     * Magic methods
     */

    /**
     * Render the content as XML
     *
     * @return string The XML string representation of the manifest
     */
    public function __toString()
    {
        $xml = new Converter($this->getStructure());

        return (string)$xml;
    }

    /**
     * Public API
     */

    /**
     * Get the manifest structure
     *
     * @return array
     */
    public function getStructure()
    {
        $root = version_compare($this->target, 1.5, '>') ? 'extension' : 'install';
        $data = $this->getManifestRoot($root);

        $this->addMetadata($data[$root]);

        foreach ($this->sections as $tag => $section) {
            $element       = $section->getAttributes();
            $element[$tag] = $section->getStructure();
            $data[$root][] = $element;
        }

        $this->addInstallHooks($data[$root]);

        return $data;
    }

    /**
     * Get the attributes for the manifest
     *
     * @return array
     */
    public function getAttributes()
    {
        return [
            '@type'    => $this->getType(),
            '@version' => $this->getTarget(),
            '@method'  => $this->getMethod(),
        ];
    }

    /**
     * Get the manifest root element
     *
     * @param string $tag The root tag (version dependent)
     *
     * @return array
     */
    protected function getManifestRoot($tag)
    {
        $data       = $this->getAttributes();
        $data[$tag] = [];

        return $data;
    }

    /**
     * Add the meta data to the structure
     *
     * @param array &$data The current structure
     *
     * @return $this This object, to provide a fluent interface
     */
    protected function addMetadata(&$data)
    {
        if (empty($this->creationDate)) {
            $this->setCreationDate();
        }

        $this->addElement($data, 'name');
        $this->addElement($data, 'author');
        $this->addElement($data, 'creationDate');
        $this->addElement($data, 'copyright');
        $this->addElement($data, 'license');
        $this->addElement($data, 'authorEmail');
        $this->addElement($data, 'authorUrl');
        $this->addElement($data, 'version');
        $this->addElement($data, 'description');

        return $this;
    }

    /**
     * Add the install hooks to the structure
     *
     * @param array &$data The current structure
     *
     * @return $this This object, to provide a fluent interface
     */
    protected function addInstallHooks(&$data)
    {
        $this->addElement($data, 'installfile');
        $this->addElement($data, 'uninstallfile');
        $this->addElement($data, 'scriptfile');

        return $this;
    }

    /**
     * Section interface
     */

    /**
     * Add a single element to the structure
     *
     * Only non-empty elements will be added.
     *
     * @param array  &$data The current structure
     * @param string  $key  The meta data field
     *
     * @return $this This object, to provide a fluent interface
     */
    protected function addElement(&$data, $key)
    {
        $value = preg_replace('~\s+~', ' ', $this->{'get' . ucfirst($key)}());

        if (!empty($value)) {
            $data[] = [$key => $value];
        }

        return $this;
    }

    /**
     * Get the manifest type
     *
     * @return string This attribute describes the type of the extension for the installer.
     *                Based on this type further requirements to sub-tags apply.
     */
    public function getType()
    {
        return $this->type;
    }

    /**
     * Manifest type cannot be set
     * Method is included for symmetry reasons.
     *
     * @throws BadMethodCallException
     */
    public function setType()
    {
        throw new BadMethodCallException('Manifest type cannot be set.');
    }

    /**
     * Get the target version
     *
     * @return string String that identifies the version of Joomla for which this extension is developed.
     */
    public function getTarget()
    {
        return $this->target;
    }

    /**
     * Set the target version
     *
     * @param string $version String that identifies the version of Joomla for which this extension is developed.
     *
     * @return $this This object, to provide a fluent interface
     */
    public function setTarget($version)
    {
        $this->target = $version;

        return $this;
    }

    /**
     * Get the installation method
     *
     * @return string Installation method, one of 'install', 'upgrade'
     *                The 'install' value means the installer will gracefully stop
     *                if it finds any existing file/folder of the new extension.
     */
    public function getMethod()
    {
        return $this->method;
    }

    /**
     * Getter and setter
     */

    /**
     * Set the installation method
     *
     * @param string $method Installation method, one of 'install', 'upgrade'
     *                       The 'install' value means the installer will gracefully stop
     *                       if it finds any existing file/folder of the new extension.
     *
     * @return $this This object, to provide a fluent interface
     * @throws InvalidArgumentException
     */
    public function setMethod($method)
    {
        $allowed = ['install', 'upgrade'];

        if (!in_array($method, $allowed, true)) {
            throw new InvalidArgumentException('Method must be one of ' . implode('|', $allowed) . '.');
        }

        $this->method = $method;

        return $this;
    }

    /**
     * Load a manifest file
     *
     * @param string $file The manifest filename
     *
     * @return Manifest
     */
    public static function load($file)
    {
        $xml       = new Converter($file);
        $type      = empty($xml['@type']) ? 'language' : $xml['@type'];
        $classname = '\\GreenCape\\Manifest\\' . ucfirst($type) . 'Manifest';

        return new $classname($xml);
    }

    /**
     * Set the manifest values and sections from XML
     *
     * @param Converter $xml
     *
     * @return $this This object, to provide a fluent interface
     * @throws UnexpectedValueException on unsupported attributes
     */
    protected function set(Converter $xml)
    {
        // Remove type and comments, if any
        unset($xml['#comment'], $xml['@type']);

        // Get attributes
        foreach ($xml as $key => $value) {
            if ($key[0] !== '@') {
                continue;
            }

            $attribute = trim(substr($key, 1));

            if (empty($attribute)) {
                continue;
            }

            if ($attribute === 'version') {
                $attribute = 'target';
            }

            $method = 'set' . ucfirst($attribute);

            if (!is_callable([$this, $method])) {
                throw new UnexpectedValueException("Can't handle attribute '$attribute'");
            }

            $this->$method($value);
        }

        // Get children
        foreach ($xml as $tag => $root) {
            if (strpos($tag, '@') === 0) {
                continue;
            }

            foreach ((array)$root as $section) {
                $key = $value = null;

                foreach ((array)$section as $key => $value) {
                    if ($key[0] !== '@' && $key[0] !== '#') {
                        break;
                    }
                }

                if ($key === 'copyright') {
                    if (preg_match('~^\D*(\d{4})[ \d-]+(.*?)(?:\.?\s+All rights reserved\.)?$~', $value, $match)) {
                        $this->setCopyright($match[1], preg_replace('~\s+~', ' ', trim($match[2])));
                    }
                } else {
                    if (isset($this->map[$key])) {
                        $classname = '\\GreenCape\\Manifest\\' . $this->map[$key];
                        $this->removeSection($key);
                        $this->addSection($key, new $classname($section));
                        continue;
                    }

                    $method = 'set' . ucfirst($key);

                    if (!is_callable([$this, $method])) {
                        throw new UnexpectedValueException("Can't handle section '$key'");
                    }

                    $this->$method($value);
                }
            }
        }

        return $this;
    }

    /**
     * Set the copyright information
     *
     * If the copyright year is different from the current year, and $createRange
     * is set to true, the copyright year is expanded to a range
     * <copyright year> - <current year>.
     *
     * The creation date is preset to the copyright year, if not already set.
     * The author is preset to the copyright owner, if not already set.
     *
     * @param string $year        The year for the copyright statement (e.g. 2011)
     * @param string $owner       The owner of the copyright (e.g. Open Source Matters)
     * @param bool   $createRange Whether or not to create a range of years (default: true)
     *
     * @return $this This object, to provide a fluent interface
     */
    public function setCopyright($year, $owner, $createRange = true)
    {
        $this->copyrightYear = $year;

        if (empty($this->creationDate)) {
            $this->creationDate = $year;
        }

        if ($createRange && $this->copyrightYear !== date('Y')) {
            $this->copyrightYear .= ' - ' . date('Y');
        }

        $this->copyrightOwner = $owner;

        if (empty($this->author)) {
            $this->author = $owner;
        }

        return $this;
    }

    /**
     * Remove a section from the manifest
     *
     * @param string $tag The tag of the section
     *
     * @return $this This object, to provide a fluent interface
     */
    public function removeSection($tag)
    {
        unset($this->sections[$tag]);

        return $this;
    }

    /**
     * Add a section to the manifest
     *
     * @param string  $tag     The tag of the section
     * @param Section $section The content of the section
     *
     * @return $this This object, to provide a fluent interface
     */
    public function addSection($tag, Section $section)
    {
        $this->sections[$tag] = $section;

        return $this;
    }

    /**
     * Get a section from the manifest
     *
     * @param string $tag The tag of the section
     *
     * @return Section The content of the section
     */
    public function getSection($tag)
    {
        return $this->sections[$tag];
    }

    /**
     * Get the extension name
     *
     * @return string Raw component name (e.g. com_banners). This is a translatable field.
     */
    public function getName()
    {
        return $this->name;
    }

    /**
     * Set the extension name
     *
     * The description is preset to "<name>_XML_DESCRIPTION", if not already set.
     *
     * @param string $name Raw component name (e.g. com_banners). This is a translatable field.
     *
     * @return $this This object, to provide a fluent interface
     */
    public function setName($name)
    {
        $this->name = $name;

        if (empty($this->description)) {
            $this->description = strtoupper("{$name}_XML_DESCRIPTION");
        }

        return $this;
    }

    /**
     * Get the author's name
     *
     * @return string Author's name (e.g. Joomla! Project)
     */
    public function getAuthor()
    {
        return $this->author;
    }

    /**
     * Set the author's name
     *
     * The copyright owner is preset to "<name>", if not already set.
     *
     * @param string $author Author's name (e.g. Joomla! Project)
     *
     * @return $this This object, to provide a fluent interface
     */
    public function setAuthor($author)
    {
        $this->author = $author;

        if (empty($this->copyrightOwner)) {
            $this->copyrightOwner = $author;
        }

        return $this;
    }

    /**
     * Get the creation date
     *
     * @return string Date of creation or release (e.g. April 2006)
     */
    public function getCreationDate()
    {
        return $this->creationDate;
    }

    /**
     * Set the creation date
     *
     * The copyright year is preset to the year of the date, if not already set.
     *
     * @param string $creationDate Date of creation or release (e.g. April 2006). Defaults to 'today'.
     *                             Any input recognized by strtotime() can be used.
     *
     * @return $this This object, to provide a fluent interface
     */
    public function setCreationDate($creationDate = 'today')
    {
        $datetime           = strtotime($creationDate);
        $this->creationDate = date('F Y', $datetime);

        if (empty($this->copyrightYear)) {
            $this->setCopyright(date('Y', $datetime), $this->copyrightOwner);
        }

        return $this;
    }

    /**
     * Get the copyright statement
     *
     * '(C)' is prepended and 'All rights reserved.' is appended automatically.
     *
     * @return string The copyright string
     */
    public function getCopyright()
    {
        return "(C) {$this->copyrightYear} {$this->copyrightOwner}. All rights reserved.";
    }

    /**
     * Get the license statement
     *
     * @return string A license statement
     */
    public function getLicense()
    {
        return $this->license;
    }

    /**
     * Set the license statement
     *
     * The license statement is preset with 'GNU General Public License version 2 or later; see LICENSE.txt'.
     *
     * @param string $license A license statement
     *
     * @return $this This object, to provide a fluent interface
     */
    public function setLicense($license)
    {
        $this->license = $license;

        return $this;
    }

    /**
     * Get the author's email address
     *
     * @return string Author's email address (e.g. admin@joomla.org)
     */
    public function getAuthorEmail()
    {
        return $this->authorEmail;
    }

    /**
     * Set the author's email address
     *
     * @param string $authorEmail Author's email address (e.g. admin@joomla.org)
     *
     * @return $this This object, to provide a fluent interface
     */
    public function setAuthorEmail($authorEmail)
    {
        $this->authorEmail = $authorEmail;

        return $this;
    }

    /**
     * Get the author's URL
     *
     * @return string URL to the author's website (e.g. www.joomla.org)
     */
    public function getAuthorUrl()
    {
        return $this->authorUrl;
    }

    /**
     * Set the author's URL
     *
     * @param string $authorUrl URL to the author's website (e.g. www.joomla.org)
     *
     * @return $this This object, to provide a fluent interface
     */
    public function setAuthorUrl($authorUrl)
    {
        $this->authorUrl = $authorUrl;

        return $this;
    }

    /**
     * Get the extension's version number
     *
     * @return string The version number of the extension (e.g. 1.6.0)
     */
    public function getVersion()
    {
        return $this->version;
    }

    /**
     * Set the extension's version number
     *
     * @param string $version The version number of the extension (e.g. 1.6.0)
     *
     * @return $this This object, to provide a fluent interface
     */
    public function setVersion($version)
    {
        $this->version = $version;

        return $this;
    }

    /**
     * Get the description
     *
     * @return string The description of the extension. This is a translatable field. (e.g. COM_BANNERS_XML_DESCRIPTION)
     */
    public function getDescription()
    {
        return $this->description;
    }

    /**
     * Set the description
     *
     * @param string $description The description of the extension. This is a translatable field. (e.g.
     *                            COM_BANNERS_XML_DESCRIPTION)
     *
     * @return $this This object, to provide a fluent interface
     */
    public function setDescription($description)
    {
        $this->description = $description;

        return $this;
    }

    /**
     * Get the install file
     *
     * Deprecated in Joomla! 1.6
     *
     * @return string The name of the file
     */
    public function getInstallFile()
    {
        return $this->installFile;
    }

    /**
     * Set the install file
     *
     * Deprecated in Joomla! 1.6
     *
     * @param string $installFile The name of the file
     *
     * @return $this This object, to provide a fluent interface
     */
    public function setInstallFile($installFile)
    {
        $this->installFile = $installFile;

        return $this;
    }

    /**
     * Get the uninstall file
     *
     * Deprecated in Joomla! 1.6
     *
     * @return string The name of the file
     */
    public function getUninstallFile()
    {
        return $this->uninstallFile;
    }

    /**
     * Set the uninstall file
     *
     * Deprecated in Joomla! 1.6
     *
     * @param string $uninstallFile The name of the file
     *
     * @return $this This object, to provide a fluent interface
     */
    public function setUninstallFile($uninstallFile)
    {
        $this->uninstallFile = $uninstallFile;

        return $this;
    }

    /**
     * Get the install/update/uninstall script
     *
     * New in Joomla! 1.6
     *
     * @return string The name of the file
     */
    public function getScriptFile()
    {
        return $this->scriptFile;
    }

    /**
     * Set the install/update/uninstall script
     *
     * New in Joomla! 1.6
     *
     * @param string $scriptFile The name of the file
     *
     * @return $this This object, to provide a fluent interface
     */
    public function setScriptFile($scriptFile)
    {
        $this->scriptFile = $scriptFile;

        return $this;
    }
}