<?php

namespace Scriptotek\SimpleMarcParser;

use Carbon\Carbon;
use Danmichaelo\QuiteSimpleXmlElement\QuiteSimpleXmlElement;

/**
 * @property int       $id                 Local record identifier
 * @property string    $material
 * @property boolean   $electronic
 * @property boolean   $is_series
 * @property boolean   $is_multivolume
 * @property boolean
 * @property string    $agency
 * @property string    $lccn
 * @property string    $catalogingRules
 * @property string    $title
 * @property string    $alternativeTitles
 * @property string    $part_no
 * @property string    $part_name
 * @property string    $part_of
 * @property string    $medium
 * @property string    $edition
 * @property string    $placeOfPublication
 * @property string    $publisher
 * @property string    $extent
 * @property string    $contents
 * @property string    $summary
 * @property string    $cover_image
 * @property string    $description
 * @property string    $preceding
 * @property string    $succeeding
 * @property string[]  $isbns
 * @property string[]  $issns
 * @property string[]  $notes
 * @property array     $series
 * @property array     $other_form
 * @property array     $creators
 * @property array     $meetings
 * @property array     $subjects
 * @property array     $genres
 * @property array     $classifications
 * @property array     $debug
 * @property int       $pages
 * @property int       $year
 * @property Carbon    $modified
 * @property Carbon    $created
 */
class BibliographicRecord extends Record
{
    /**
     * @param string $x1
     * @param string $x2
     * @param string $default
     *
     * @return string
     */
    public function getMaterialSubtypeFrom007($x1, $x2, $default = 'Unknown')
    {
        $f007values = array(
            'a' => array(
                'd' => 'Atlas',
                'g' => 'Diagram',
                'j' => 'Map',
                'k' => 'Profile',
                'q' => 'Model',
                'r' => 'Remote-sensing image',
                '_' => 'Map',
            ),
            'c' => array(
                'a' => 'Tape cartridge',
                'b' => 'Chip cartridge',
                'c' => 'Computer optical disc cartridge',
                'd' => 'Computer disc, type unspecified',
                'e' => 'Computer disc cartridge, type unspecified',
                'f' => 'Tape cassette',
                'h' => 'Tape reel',
                'j' => 'Magnetic disk',
                'k' => 'Computer card',
                'm' => 'Magneto-optical disc',
                'o' => 'CD-ROM',                // Optical disc
                'r' => 'Remote resource',    // n Nettdokumenter
            ),
            'f' => array(
                'a' => 'Moon',         // in the Moon writing system
                'b' => 'Braille',      // in the Braille writing system
                'c' => 'Combination ', // in a combination of two or more of the other defined types
                'd' => 'No writing system',
            ),
            'h' => array(
                'a' => 'Aperture card',
                'b' => 'Microfilm cartridge',
                'c' => 'Microfilm cassette',
                'd' => 'Microfilm reel',
                'e' => 'Microfiche',
                'f' => 'Microfiche cassette',
                'g' => 'Microopaque',
                'h' => 'Microfilm slip',
                'j' => 'Microfilm roll',
                'u' => 'Microform', // Unspecified
                'z' => 'Microform', // Other
                '|' => 'Microform', // No attempt to code
            ),
            'o' => array(
                'u' => 'Kit',
                '|' => 'Kit',
            ),
            's' => array(
                'd' => 'Music CD',             // v CD-er
                'e' => 'Cylinder',
                'g' => 'Sound cartridge',
                'i' => 'Sound-track film',
                'q' => 'Roll',
                's' => 'Sound cassette',
                't' => 'Sound-tape reel',
                'u' => 'Unspecified',
                'w' => 'Wire recording',
            ),
            'v' => array(
                'c' => 'Videocartridge',
                'd' => 'Videodisc',           // w DVD-er
                'f' => 'Videocassette',
                'r' => 'Videoreel',
            ),
        );
        if (isset($f007values[$x1]) && isset($f007values[$x1][$x2])) {
            return $f007values[$x1][$x2];
        }
        // TODO: LOG IT!
        return $default;
    }

    /**
     * @param \Danmichaelo\QuiteSimpleXMLElement\QuiteSimpleXMLElement $data
     */
    protected function parseMaterial(QuiteSimpleXmlElement $data)
    {
        // See http://www.loc.gov/marc/ldr06guide.html

        // The Leader/06 (Type of record) character position contains a one-character
        // alphabetic code that differentiates MARC records created for various types of
        // content and materials.

        // The 008 field contains character positions that provide coded information about
        // the record as a whole and about special bibliographic aspects of the item cataloged.

        // Field 006 (Fixed-length data elements - Additional material characteristics)
        // permits coding for additional aspects of an electronic resource (including any
        // computer file aspects) if the 007 and 008 fields do not adequately describe it.

        // leader[6] : Type of record
        // leader[7] : Bibliographic level
        // Field 008/18-34 Configuration

        // LDR/06
        $recordTypes = array(
            'a' => 'Language material',
            'c' => 'Notated music',
            'd' => 'Manuscript notated music',
            'e' => 'Cartographic material',
            'f' => 'Manuscript cartographic material',
            'g' => 'Projected medium',
            'i' => 'Nonmusical sound recording',
            'j' => 'Musical sound recording',
            'k' => 'Two-dimensional nonprojectable graphic',
            'm' => 'Computer file',
            'o' => 'Kit',
            'p' => 'Mixed materials',
            'r' => 'Three-dimensional artifact or naturally occurring object',
            't' => 'Manuscript language material',
        );

        // LDR/07
        $bibliographicLevels = array(
            'a' => 'Monographic component part',
            'b' => 'Serial component part',
            'c' => 'Collection',
            'd' => 'Subunit',
            'i' => 'Integrating resource',
            'm' => 'Monograph/Item',
            's' => 'Serial',
        );

        // 007/00 (Category of material)
        $materialCategories = array(
            'a' => 'Map',
            'c' => 'Electronic resource',
            'd' => 'Globe',
            'f' => 'Tactile material',
            'g' => 'Projected graphic',
            'h' => 'Microform',
            'k' => 'Nonprojected graphic',
            'm' => 'Motion picture',
            'o' => 'Kit',
            'q' => 'Notated music',
            'r' => 'Remote-sensing image',
            's' => 'Sound recording',
            't' => 'Text',                 // p Trykt materiale
            'v' => 'Videorecording',
            'z' => 'Unspecified',
        );

        // 008/24-27 - Nature of contents (006/07-10)
        $natureOfContents = array(
            'a' => 'Abstract',
            'b' => 'Bibliography',
            'c' => 'Catalog',
            'd' => 'Dictionary',
            'e' => 'Encyclopedia',
            'f' => 'Handbook',
            'g' => 'Legal article',
            'i' => 'Index',
            'j' => 'Patent document',
            'k' => 'Discography',
            'l' => 'Legislation',
            'm' => 'Thesis',
            'n' => 'Surveys of literature in a subject area',
            'o' => 'Review',
            'p' => 'Programmed text',
            'q' => 'Filmography',
            'r' => 'Directory',
            's' => 'Statistics',
            't' => 'Technical report',
            'u' => 'Standards/specification',
            'v' => 'Legal cases and case notes',
            'w' => 'Law reports and digests',
            'y' => 'Yearbook',
            'z' => 'Treaty',
            '2' => 'Offprint',
            '5' => 'Calendar',
            '6' => 'Comics/graphic novel',
        );

        $videoFormats = array(
            'a' => 'Beta (1/2 in., videocassette)',
            'b' => 'VHS (1/2 in., videocassette)',
            'c' => 'U-matic (3/4 in., videocasstte)',
            'd' => 'EIAJ (1/2 in., reel)',
            'e' => 'Type C (1 in., reel)',
            'f' => 'Quadruplex (1 in. or 2 in., reel)',
            'g' => 'Laserdisc',
            'h' => 'CED (Capacitance Electronic Disc) videodisc',
            'i' => 'Betacam (1/2 in., videocassette)',
            'j' => 'Betacam SP (1/2 in., videocassette)',
            'k' => 'Super-VHS (1/2 in., videocassette)',
            'm' => 'M-II (1/2 in., videocassette)',
            'o' => 'D-2 (3/4 in., videocassette)',
            'p' => '8 mm.',
            'q' => 'Hi-8 mm.',
            's' => 'Blu-ray',
            'u' => 'Unknown',
            'v' => 'DVD',
        );

        // If Leader/06 = a and Leader/07 = a, c, d, or m: Books
        // If Leader/06 = a and Leader/07 = b, i, or s: Continuing Resources
        // If Leader/06 = t: Books
        // If Leader/06 = c, d, i, or j: Music
        // If Leader/06 = e, or f: Maps
        // If Leader/06 = g, k, o, or r: Visual Materials
        // If Leader/06 = m: Computer Files
        // If Leader/06 = p: Mixed Materials

        $ldr = str_split($data->text('marc:leader'));
        $f007 = str_split($data->text('marc:controlfield[@tag="007"]'));
        $f008 = str_split($data->text('marc:controlfield[@tag="008"]'));

        $material = 'Unknown';
        $this->material = $material;
        $this->electronic = false;

        $this->debug = array(
            'ldr06' => array_get($ldr, 6),
            'ldr07' => array_get($ldr, 7),
            'f7_01' => array_get($f007, 0) ?: null,
            'f7_02' => array_get($f007, 1),
        );

        if (count($ldr) < 8) {
            return;
        }
        if (count($f007) < 2) {
            return;
        }

        switch ($ldr[6]) {

            case 'a':
                if (in_array($ldr[7], array('a', 'c', 'd', 'm'))) {
                    $material = 'Book';
                }
                if (in_array($ldr[7], array('b', 'i', 's'))) {
                    $material = 'Series';
                }
                break;

            case 't':
                $material = 'Book';
                break;

            case 'c':
            case 'd':
            case 'i':
            case 'j':
                $material = 'Music';
                break;

            case 'e':
            case 'f':
                $material = 'Map';
                break;

            case 'g':
            case 'k':
            case 'o':
            case 'r':
                $material = 'Visual';
                break;

            case 'm':
                $material = 'File';
                // used for computer software, numeric data, not for e-books or e-journals!
                break;

            case 'p':
                $material = 'Mixed';
                break;

        }

        $online = ($f007[0] == 'c' && $f007[1] == 'r');

        if ($material == 'File') {
            $material = $this->getMaterialSubtypeFrom007($f007[0], $f007[1], $material);
        } elseif ($material == 'Visual') {
            $material = $this->getMaterialSubtypeFrom007($f007[0], $f007[1], $material);

            if (isset($f007[4]) && isset($videoFormats[$f007[4]])) {
                $material = $videoFormats[$f007[4]]; // DVD, Blu-ray
            }
        } elseif ($material == 'Music') {
            if ($f007[0] == 't') {
                $material = 'Sheet music';
            } else {
                $material = $this->getMaterialSubtypeFrom007($f007[0], $f007[1], $material);
                if (array_get($ldr, 7) == 'a') {
                    $material .= ' track';
                }
            }
        } elseif ($material == 'Series') {
            switch ($f008[21]) {

                case 'm':
                    $material = 'Series';  // Monographic series (merk: skiller ikke mellom 'flerbindsverk' og 'serieinnførsel')
                    break;

                case 'n':
                    $material = 'Newspaper';
                    if ($f007[0] == 'h') {
                        $material .= ' on microform';
                    }
                    break;

                case 'p':
                    $material = 'Periodical';
                    break;

            }
            if (array_get($ldr, 7) == 'a') {
                $material = 'Article';
            }
        } elseif ($material == 'Book') {
            if (isset($natureOfContents[$f008[24]])) {
                // Slight simplification
                $material = $natureOfContents[$f008[24]];
            }
            if (array_get($ldr, 7) == 'a') {
                $material = 'Article';
                // or Article/Chapter ?
            }
        }

        $this->material = $material;
        $this->electronic = $online;
    }

    public function addClassification($node, &$classifications, $fields, $system = null, $edition = null, $assigner = null)
    {
        $cl = array('system' => $system, 'edition' => $edition, 'number' => null, 'assigner' => $assigner);
        foreach ($fields as $key => $val) {
            $t = $node->text('marc:subfield[@code="' . $key . '"]');
            if (!is_array($val)) {
                $val = array($val);
            }
            if (count($val) > 2) {
                $t = preg_replace('/' . $val[1] . '/', $val[2], $t);
            }
            if (!empty($t)) {
                $cl[$val[0]] = $t;
            }
        }

        if (is_null($cl['system'])) {
            return;
            // Invalid value in $a, should we trigger some event to allow logging?
        }

        if (is_null($cl['number'])) {
            return;
            // Invalid value in $a, should we trigger some event to allow logging?
        }

        $classifications[] = $cl;
    }

    /**
     * Parses common elements in subject added entry fields 600-655.
     *
     * @param \Danmichaelo\QuiteSimpleXMLElement\QuiteSimpleXMLElement $node
     *
     * @return array
     */
    public function parseSubjectAddedEntry(QuiteSimpleXmlElement &$node)
    {
        $out = array('term' => '', 'vocabulary' => null);
        $vocabularies = array(
            '0' => 'lcsh',
            '1' => 'lccsh', // LC subject headings for children's literature
            '2' => 'mesh', // Medical Subject Headings
            '3' => 'atg', // National Agricultural Library subject authority file (?)
            // 4 : unknown
            '5' => 'cash', // Canadian Subject Headings
            '6' => 'rvm', // Répertoire de vedettes-matière
            // 7: Source specified in subfield $2
        );
        $ind2 = $node->attr('ind2');

        $id = $node->text('marc:subfield[@code="0"]');
        $out['id'] = empty($id) ? null : $id;

        if (isset($vocabularies[$ind2])) {
            $out['vocabulary'] = $vocabularies[$ind2];
        } elseif ($ind2 == '7') {
            $vocab = $node->text('marc:subfield[@code="2"]');
            if (!empty($vocab)) {
                $out['vocabulary'] = $vocab;
            }
        } elseif ($ind2 == '4') {
            $this->parseAuthority($node->text('marc:subfield[@code="0"]'), $out);
        }

        $out['parts'] = array();
        $subdivtypes = array(
            'v' => 'form',
            'x' => 'general',
            'y' => 'chronologic',
            'z' => 'geographic',
        );
        foreach ($node->all('marc:subfield') as $subdiv) {
            $code = $subdiv->attr('code');
            if (in_array($code, array_keys($subdivtypes))) {
                $subdiv = trim($subdiv, '.');
                $out['parts'][] = array('value' => $subdiv, 'type' => $subdivtypes[$code]);
                $out['term'] .= self::$subfieldSeparator . $subdiv;
            }
        }

        return $out;
    }

    /**
     * @param \Danmichaelo\QuiteSimpleXMLElement\QuiteSimpleXMLElement $data
     */
    public function __construct(QuiteSimpleXmlElement $data = null)
    {
        if (is_null($data)) {
            return;
        }

        $this->parseMaterial($data);

        // Control fields
        $this->id = $data->text('marc:controlfield[@tag="001"]');

        // 003: MARC code for the agency whose system control number is
        // contained in field 001 (Control Number)
        // See http://www.loc.gov/marc/authority/ecadorg.html
        $this->agency = $data->text('marc:controlfield[@tag="003"]');

        // 005: Modified
        $this->modified = $this->parseDateTime($data->text('marc:controlfield[@tag="005"]'));

        // 008: Extract *some* information
        $f008 = $data->text('marc:controlfield[@tag="008"]');
        $this->created = $this->parseDateTime(substr($f008, 0, 6));
        $this->language = substr($f008, 35, 3);

        $creators = array();
        $meetings = array();
        $subjects = array();
        $genres = array();
        $classifications = array();
        $series = array();
        $notes = array();
        $isbns = array();
        $issns = array();
        $alternativeTitles = array();

        // Relationships
        $preceding = array();
        $succeeding = array();
        $part_of = array();
        $other_form = array();

        $this->is_series = false;
        $this->is_multivolume = false;

        foreach ($data->all('marc:datafield') as $node) {
            $marcfield = intval($node->attributes()->tag);
            switch ($marcfield) {

                // 010 - Library of Congress Control Number (NR)
                case 10:
                    $this->lccn = $node->text('marc:subfield[@code="a"]');
                    break;

                // 020 - International Standard Book Number (R)
                case 20:                                                            // Test added
                    $isbn = $node->text('marc:subfield[@code="a"]');
                    $isbn = preg_replace('/^([0-9\-xX]+).*$/', '\1', $isbn);
                    if (empty($isbn)) {
                        break;
                    }
                    array_push($isbns, $isbn);
                    break;

                // 022 - International Standard Serial Number (R)
                case 22:                                                            // Test added
                    $issn = $node->text('marc:subfield[@code="a"]');
                    $issn = preg_replace('/^([0-9\-xX]+).*$/', '\1', $issn);
                    if (empty($issn)) {
                        break;
                    }
                    array_push($issns, $issn);
                    break;

                // 040 - Cataloging Source (NR)
                case 40:                                                            // Test added
                    $x = $node->text('marc:subfield[@code="e"]');
                    if ($x) {
                        $this->catalogingRules = $x;
                    }
                    // Value from http://www.loc.gov/standards/sourcelist/descriptive-conventions.html
                    break;

                // 060 - National Library of Medicine Call Number (R)
                case 60:
                    $this->addClassification(
                        $node,
                        $classifications,
                        array(
                            'a' => 'number',
                        ),
                        $system = 'nlm',
                        $assigner = ($node->attr('ind2') == '0') ? 'DNLM' : null
                    );
                    break;

                // 080 - Universal Decimal Classification Number (R)
                case 80:
                    $this->addClassification(
                        $node,
                        $classifications,
                        array(
                            'a' => array('number', '^.*?([0-9.\/:()]+).*$', '\1'),
                            '2' => 'edition',
                        ),
                        $system = 'udc'
                    );
                    break;

                // 082 - Dewey Decimal Classification Number (R)
                case 82:
                    $this->addClassification(
                        $node,
                        $classifications,
                        array(
                            'a' => array('number', '^.*?([0-9.]+)\/?([0-9.]*).*$', '\1\2'),
                            '2' => 'edition',
                            'q' => 'assigner',
                        ),
                        $system = 'ddc'
                    );
                    break;

                // 084 - Other Classification Number (R)
                case 84:
                    $this->addClassification(
                        $node,
                        $classifications,
                        array(
                            'a' => 'number',
                            '2' => 'system',
                            'q' => 'assigner',
                        )
                    );
                    break;

                /*
                case 89:
                    if (!isset($this->klass)) $this->klass = array();
                    $klass = $node->text('marc:subfield[@code="a"]');
                    $klass = preg_replace('/[^0-9.]/', '', $klass);
                    foreach ($this->klass as $kitem) {
                        if (($kitem['kode'] == $klass) && ($kitem['system'] == 'dewey')) {
                            continue 3;
                        }
                    }
                    array_push($this->klass, array('kode' => $klass, 'system' => 'dewey'));
                    break;
                */

                case 100:
                    $author = array(
                        'name' => $node->text('marc:subfield[@code="a"]'),
                    );
                    $author['normalizedName'] = $author['name'];
                    $spl = explode(', ', $author['name']);
                    if (count($spl) == 2) {
                        $author['name'] = $spl[1] . ' ' . $spl[0];
                    }
                    $this->parseRelator($node, $author, 'main');
                    $this->parseAuthority($node->text('marc:subfield[@code="0"]'), $author);

                    $creators[] = $author;
                    break;

                case 110:
                    $author = array(
                        'name' => $node->text('marc:subfield[@code="a"]'),
                    );
                    $author['normalizedName'] = $author['name'];
                    foreach ($node->all('marc:subfield[@code="b"]') as $subunit) {
                        $author['name'] .= self::$subfieldSeparator . trim($subunit, ',');
                    }
                    $this->parseRelator($node, $author, 'corporate');
                    $this->parseAuthority($node->text('marc:subfield[@code="0"]'), $author);

                    $creators[] = $author;
                    break;

                case 111:
                    $meeting = array(
                        'name' => $node->text('marc:subfield[@code="a"]'),
                    );
                    $meeting['normalizedName'] = $meeting['name'];
                    $this->parseRelator($node, $meeting, 'meeting');
                    $this->parseAuthority($node->text('marc:subfield[@code="0"]'), $meeting);

                    $meetings[] = $meeting;
                    break;

                case 130:
                    // Uniform title

                    // TODO: Need a more robust way to prefer 130 over 245
                    //       Currently we depend on 130 coming before 245.
                    $this->title = $node->text('marc:subfield[@code="a"]');
                    break;

                // 245 : Title Statement (NR)
                case 245:
                    $title = $node->text('marc:subfield[@code="a"]') . ' ' . $node->text('marc:subfield[@code="b"]');
                    $title = rtrim($title, ' /');

                    $titleParts = preg_split('/(:|=)/', $title, -1, PREG_SPLIT_DELIM_CAPTURE);
                    $isbdCode = ':';
                    $title = '';
                    foreach ($titleParts as $titlePart) {
                        $titlePart = trim($titlePart);
                        if (is_null($isbdCode)) {
                            $isbdCode = $titlePart;
                        } else {
                            if ($isbdCode == '=') {
                                $alternativeTitles[] = $titlePart;
                            } else { // isbdCode == ':' or ';'
                                // TODO: Cover more cases explicitly?
                                //  http://www.loc.gov/marc/bibliographic/bd245.html
                                $title = empty($title) ? $titlePart : "$title $isbdCode $titlePart";
                            }
                            $isbdCode = null;
                        }
                    }

                    if (isset($this->title)) { // If not already set by 130
                        $alternativeTitles[] = $title;
                    } else {
                        $this->title = $title;
                    }

                    // $n : Number of part/section of a work (R)
                    $part_no = $node->text('marc:subfield[@code="n"]');
                    if ($part_no !== '') {
                        $this->part_no = $part_no;
                    }

                    // $p : Name of part/section of a work (R)
                    $part_name = $node->text('marc:subfield[@code="p"]');
                    if ($part_name !== '') {
                        $this->part_name = $part_name;
                    }

                    // $h : Medium (NR)
                    $medium = $node->text('marc:subfield[@code="h"]');
                    if ($medium !== '') {
                        $this->medium = $medium;
                    }

                    break;

                // 246 : Varying Form of Title (R)
                case 246:
                    // Note: The second indicator gives the type of title:
                    // 0 - Portion of title
                    // 1 - Parallel title
                    // 2 - Distinctive title
                    // 3 - Other title
                    // 4 - Cover title
                    // 5 - Added title page title
                    // 6 - Caption title
                    // 7 - Running title
                    // 8 - Spine title
                    $title = rtrim($node->text('marc:subfield[@code="a"]'), ' :-');
                    $subtitle = $node->text('marc:subfield[@code="b"]');
                    // TODO: Cover more of the cases on
                    //  http://www.loc.gov/marc/bibliographic/bd246.html
                    if (!empty($subtitle)) {
                        $title .= ' : ' . $subtitle;
                    }
                    $alternativeTitles[] = $title;
                    break;

                case 250:
                    $this->edition = $node->text('marc:subfield[@code="a"]');
                    break;

                case 260:
                    $this->placeOfPublication = $node->text('marc:subfield[@code="a"]');
                    $this->publisher = $node->text('marc:subfield[@code="b"]');
                    $y = preg_replace('/^.*?([0-9]{4}).*$/', '\1', $node->first('marc:subfield[@code="c"]'));
                    $this->year = $y ? intval($y) : null;
                    break;

                case 300:
                    $this->extent = $node->text('marc:subfield[@code="a"]');

                    # 2.5B2 "327 s.", 2.5B4 "48 [i.e. 96] s.", 2.5B7 "[93] s."
                    preg_match(
                        '/\[?([0-9]+)\]? (s.|p.|pp.)/',
                        $node->text('marc:subfield[@code="a"]'),
                        $matches
                    );
                    if ($matches) {
                        $this->pages = intval($matches[1]);
                    }

                    # 2.5B6 Eks: "s. 327-698" (flerbindsverk)
                    preg_match(
                        '/(s.|p.|pp.) ([0-9]+)-([0-9]+)/',
                        $node->text('marc:subfield[@code="a"]'),
                        $matches
                    );
                    if ($matches) {
                        $this->pages = intval($matches[3]) - intval($matches[2]) + 1;
                    }
                    break;

                /*
                case 490:
                    $serie = array(
                        'title' => $node->text('marc:subfield[@code="a"]'),
                        'volume' => $node->text('marc:subfield[@code="v"]')
                    );
                    $this->series[] = $serie;
                    break;
                */

                // 500 : General Note (R)
                case 500:

                    // $a - General note (NR)
                    $notes[] = $node->text('marc:subfield[@code="a"]');
                    break;

                // 502 : Dissertation Note (R)
                case 502:

                    // $a - Dissertation note (NR)
                    $notes[] = $node->text('marc:subfield[@code="a"]');
                    break;

                case 505:

                    // <datafield tag="520" ind1=" " ind2=" ">
                    //     <subfield code="a">"The conceptual changes brought by modern physics are important, radical and fascinating, yet they are only vaguely understood by people working outside the field. Exploring the four pillars of modern physics - relativity, quantum mechanics, elementary particles and cosmology - this clear and lively account will interest anyone who has wondered what Einstein, Bohr, Schrödinger and Heisenberg were really talking about. The book discusses quarks and leptons, antiparticles and Feynman diagrams, curved space-time, the Big Bang and the expanding Universe. Suitable for undergraduate students in non-science as well as science subjects, it uses problems and worked examples to help readers develop an understanding of what recent advances in physics actually mean"--</subfield>
                    //     <subfield code="c">Provided by publisher.</subfield>
                    // </datafield>
                    $this->contents = $node->text('marc:subfield[@code="a"]');
                    break;

                case 520:

                    // <datafield tag="520" ind1=" " ind2=" ">
                    //     <subfield code="a">"The conceptual changes brought by modern physics are important, radical and fascinating, yet they are only vaguely understood by people working outside the field. Exploring the four pillars of modern physics - relativity, quantum mechanics, elementary particles and cosmology - this clear and lively account will interest anyone who has wondered what Einstein, Bohr, Schrödinger and Heisenberg were really talking about. The book discusses quarks and leptons, antiparticles and Feynman diagrams, curved space-time, the Big Bang and the expanding Universe. Suitable for undergraduate students in non-science as well as science subjects, it uses problems and worked examples to help readers develop an understanding of what recent advances in physics actually mean"--</subfield>
                    //     <subfield code="c">Provided by publisher.</subfield>
                    // </datafield>
                    $this->summary = array(
                        'assigning_source' => $node->text('marc:subfield[@code="c"]'),
                        'text' => $node->text('marc:subfield[@code="a"]'),
                    );
                    break;

                // 580 : Complex Linking Note (R)
                case 580:

                    if ($data->has('marc:datafield[@tag="780"]')) {
                        $preceding['note'] = $node->text('marc:subfield[@code="a"]');
                    } elseif ($data->has('marc:datafield[@tag="785"]')) {
                        $succeeding['note'] = $node->text('marc:subfield[@code="a"]');
                    } elseif ($data->has('marc:datafield[@tag="773"]')) {
                        $part_of['note'] = $node->text('marc:subfield[@code="a"]');
                    }
                    break;

                case 600:
                    $tmp = $this->parseSubjectAddedEntry($node);

                    $name = $node->text('marc:subfield[@code="a"]');
                    $qualifiers = array();
                    $titles = $node->text('marc:subfield[@code="c"]');
                    if (!empty($titles)) {
                        $qualifiers[] = trim($titles, '(),.');
                    }
                    $dates = $node->text('marc:subfield[@code="d"]');
                    if (!empty($dates)) {
                        $qualifiers[] = trim($dates, '(),.');
                    }

                    // - To concat or not concat… not sure, but in principle
                    // the $a $c $d $q values should be the same on all
                    // records, so for filtering purposes it doesn't seem
                    // necessary to have them separated. For display
                    // purposes, it can be useful of course. But then again,
                    // the parts should be available from the authority
                    // register..
                    // - Another question is whether we should do some
                    // normalizing to try aligning records with different
                    // punctuation standards (US vs UK)
                    if (count($qualifiers) != 0) {
                        $name = "$name (" . implode(', ', $qualifiers) . ')';
                    }
                    $tmp['term'] = $name . $tmp['term'];
                    $tmp['type'] = 'person';

                    array_push($subjects, $tmp);
                    break;

                case 610:
                    $tmp = $this->parseSubjectAddedEntry($node);

                    $name = trim($node->text('marc:subfield[@code="a"]'), ',');
                    foreach ($node->all('marc:subfield[@code="b"]') as $subunit) {
                        $name .= self::$subfieldSeparator . trim($subunit, ',');
                    }
                    $tmp['type'] = 'corporation';
                    $tmp['term'] = $name . $tmp['term'];
                    array_push($subjects, $tmp);
                    break;

                case 611:
                    $tmp = $this->parseSubjectAddedEntry($node);
                    $dates = $node->text('marc:subfield[@code="d"]');
                    if (!empty($dates)) {
                        $tmp['time'] = trim($dates, ' :,.()');
                    }
                    $location = $node->text('marc:subfield[@code="c"]');
                    if (!empty($location)) {
                        $tmp['place'] = trim($location, ' :,.()');
                    }
                    $misc = $node->text('marc:subfield[@code="g"]');
                    if (!empty($misc)) {
                        $tmp['misc'] = trim($misc, ' :,.()');
                    }
                    $number = $node->text('marc:subfield[@code="n"]');
                    if (!empty($number)) {
                        $tmp['number'] = trim($number, ' :,.()');
                    }

                    $name = trim($node->text('marc:subfield[@code="a"]'), ',');
                    $tmp['type'] = 'meeting';
                    $tmp['term'] = $name . $tmp['term'];
                    array_push($subjects, $tmp);
                    break;

                case 648:
                    $tmp = $this->parseSubjectAddedEntry($node);

                    $emne = $node->text('marc:subfield[@code="a"]');
                    $tmp['type'] = 'chronologic';
                    $tmp['term'] = trim($emne, '.') . $tmp['term'];

                    array_push($subjects, $tmp);
                    break;

                case 650:
                    $tmp = $this->parseSubjectAddedEntry($node);

                    $emne = $node->text('marc:subfield[@code="a"]');
                    $tmp['term'] = trim($emne, '.') . $tmp['term'];
                    $tmp['type'] = 'topic';

                    array_push($subjects, $tmp);
                    break;

                case 651:
                    $tmp = $this->parseSubjectAddedEntry($node);

                    $emne = $node->text('marc:subfield[@code="a"]');
                    $tmp['type'] = 'geographic';
                    $tmp['term'] = trim($emne, '.') . $tmp['term'];

                    array_push($subjects, $tmp);
                    break;

                case 653:
                    $tmp = array('type' => null, 'vocabulary' => null);

                    $ind2 = $node->attr('ind2');
                    $types = array(
                        '0' => 'topic',
                        '1' => 'person',
                        '2' => 'corporate',
                        '3' => 'meeting',
                        '4' => 'chronologic',
                        '5' => 'geographic',
                        '6' => 'genre',
                    );
                    if (isset($types[$ind2])) {
                        $tmp['type'] = $types[$ind2];
                    }

                    foreach ($node->all('marc:subfield[@code="a"]') as $emne) {
                        $tmp['term'] = trim($emne, '.');
                        if ($tmp['type'] == 'genre') {
                            unset($tmp['type']);
                            array_push($genres, $tmp);
                        } else {
                            array_push($subjects, $tmp);
                        }
                    }

                    break;

                case 655:

                    $tmp = $this->parseSubjectAddedEntry($node);
                    $tmp['term'] = trim($node->text('marc:subfield[@code="a"]'), '.') . $tmp['term'];

                    array_push($genres, $tmp);
                    break;

                case 700:
                    $author = array(
                        'name' => $node->text('marc:subfield[@code="a"]'),
                    );
                    $author['normalizedName'] = $author['name'];
                    $spl = explode(', ', $author['name']);
                    if (count($spl) == 2) {
                        $author['name'] = $spl[1] . ' ' . $spl[0];
                    }

                    $this->parseRelator($node, $author, 'added');
                    $this->parseAuthority($node->text('marc:subfield[@code="0"]'), $author);

                    $dates = $node->text('marc:subfield[@code="d"]');
                    if (!empty($dates)) {
                        $author['dates'] = $dates;
                    }

                    $creators[] = $author;
                    break;

                case 710:
                    $author = array(
                        'name' => $node->text('marc:subfield[@code="a"]'),
                    );
                    $author['normalizedName'] = $author['name'];

                    $this->parseRelator($node, $author, 'added_corporate');
                    $this->parseAuthority($node->text('marc:subfield[@code="0"]'), $author);

                    $creators[] = $author;
                    break;

                // 773 : Host Item Entry (R)
                // See also: 580
                case 773:
                    $part_of = isset($part_of) ? $part_of : array();
                    $part_of['relationship'] = $node->text('marc:subfield[@code="i"]');
                    $part_of['title'] = $node->text('marc:subfield[@code="t"]');
                    $part_of['issn'] = $node->text('marc:subfield[@code="x"]') ?: null;
                    $part_of['isbn'] = $node->text('marc:subfield[@code="z"]') ?: null;
                    $part_of['volume'] = $node->text('marc:subfield[@code="v"]') ?: null;
                    $this->parseAuthority($node->text('marc:subfield[@code="w"]'), $part_of);

                    break;

                // 776 : Additional Physical Form Entry (R)
                case 776:
                        // <marc:datafield tag="776" ind1="0" ind2=" ">
                        //     <marc:subfield code="z">9781107602175</marc:subfield>
                        //     <marc:subfield code="w">(NO-TrBIB)132191512</marc:subfield>
                        // </marc:datafield>
                    $other_form = $this->parseRelationship($node);
                    break;

                // 780 : Preceding Entry (R)
                // Information concerning the immediate predecessor of the target item
                case 780:
                    // <marc:datafield tag="780" ind1="0" ind2="0">
                    //     <marc:subfield code="w">(NO-TrBIB)920713874</marc:subfield>
                    //     <marc:subfield code="g">nr 80(1961)</marc:subfield>
                    // </marc:datafield>

                    if (!isset($preceding['items'])) {
                        $preceding['items'] = array();
                    }
                    $preceding['items'][] = $this->parseRelationship($node);

                    $ind2 = $node->attr('ind2');
                    $relationship_types = array(
                        '0' => 'Continues',
                        '1' => 'Continues in part',
                        '2' => 'Supersedes',
                        '3' => 'Supersedes in part',
                        '4' => 'Formed by the union of',  // ... and ...',
                        '5' => 'Absorbed',
                        '6' => 'Absorbed in part',
                        '7' => 'Separated from',
                    );
                    if (isset($relationship_types[$ind2])) {
                        $preceding['relationship_type'] = $relationship_types[$ind2];
                    }

                    break;

                // 785 : Succeeding Entry (R)
                // Information concerning the immediate successor to the target item
                case 785:
                    // <marc:datafield tag="785" ind1="0" ind2="0">
                    //     <marc:subfield code="w">(NO-TrBIB)920713874</marc:subfield>
                    //     <marc:subfield code="g">nr 80(1961)</marc:subfield>
                    // </marc:datafield>

                    if (!isset($succeeding['items'])) {
                        $succeeding['items'] = array();
                    }
                    $succeeding['items'][] = $this->parseRelationship($node);

                    $ind2 = $node->attr('ind2');
                    $relationship_types = array(
                        '0' => 'Continued by',
                        '1' => 'Continued in part by',
                        '2' => 'Superseded by',
                        '3' => 'Superseded in part by',
                        '4' => 'Absorbed by',
                        '5' => 'Absorbed in part by',
                        '6' => 'Split into',  // ... and ...',
                        '7' => 'Continued by', // more precisely 'Merged with ... to form ...', but we simplify slightly
                        '8' => 'Changed back to',
                    );

                    if ($ind2 == '7') {
                        // The first elements are the documents that go into the merge,
                        // we are not interested in those due to our slight simplification.
                        $succeeding['items'] = array(array_pop($succeeding['items']));
                    }

                    if (isset($relationship_types[$ind2])) {
                        $succeeding['relationship_type'] = $relationship_types[$ind2];
                    }
                    break;

                // 830 : Series Added Entry – Uniform Title (R)
                case 830:
                    $serie = array(
                        'title' => $node->text('marc:subfield[@code="a"]'),
                        'id' => preg_replace('/\(NO-TrBIB\)/', '', $node->text('marc:subfield[@code="w"]')) ?: null,
                        'volume' => $node->text('marc:subfield[@code="v"]') ?: null,
                    );
                    $series[] = $serie;
                    break;

                case 856:
                case 956:
                    # MARC 21 uses field 856 for electronic "links", where you can have URLs for example covers images and/or blurbs.
                    # 956 ?

                        // <marc:datafield tag="856" ind1="4" ind2="2">
                        //     <marc:subfield code="3">Beskrivelse fra forlaget (kort)</marc:subfield>
                        //     <marc:subfield code="u">http://content.bibsys.no/content/?type=descr_publ_brief&isbn=0521176832</marc:subfield>
                        // </marc:datafield>
                        // <marc:datafield tag="956" ind1="4" ind2="2">
                        //     <marc:subfield code="3">Omslagsbilde</marc:subfield>
                        //     <marc:subfield code="u">http://innhold.bibsys.no/bilde/forside/?size=mini&id=9780521176835.jpg</marc:subfield>
                        //     <marc:subfield code="q">image/jpeg</marc:subfield>
                        // </marc:datafield>
                    $description = $node->text('marc:subfield[@code="3"]');
                    if (in_array($description, array('Cover image', 'Omslagsbilde'))) {
                        $this->cover_image = $node->text('marc:subfield[@code="u"]');

                        // Silly hack to get larger images from Bibsys:
                        $this->cover_image = str_replace('mini', 'stor', $this->cover_image);
                        $this->cover_image = str_replace('LITE', 'STOR', $this->cover_image);
                    }
                    if (in_array($description, array('Beskrivelse fra forlaget (kort)', 'Beskrivelse fra forlaget (lang)'))) {
                        $this->description = $node->text('marc:subfield[@code="u"]');
                    }
                    break;

                // 991 Kriterium für Sekundärsortierung (R) ???
                // Ref: http://ead.nb.admin.ch/web/marc21/dmarcb991.pdf
                // Hvor i BIBSYSMARC kommer dette fra?
                case 991:

                    // Multi-volume work (flerbindsverk), parts linked through 773 w
                    if ($node->text('marc:subfield[@code="a"]') == 'volumes') {
                        $this->is_multivolume = true;
                    }

                    // Series (serier), parts linked through 830 w
                    if ($node->text('marc:subfield[@code="a"]') == 'parts') {
                        $this->is_series = true;
                    }

                    break;

            }
        }

        if (!empty($preceding)) {
            $this->preceding = $preceding;
        }
        if (!empty($succeeding)) {
            $this->succeeding = $succeeding;
        }
        if (count($part_of)) {
            $this->part_of = $part_of;
        }
        if (!empty($other_form)) {
            $this->other_form = $other_form;
        }

        $this->alternativeTitles = array_unique($alternativeTitles);
        $this->isbns = $isbns;
        $this->issns = $issns;
        $this->series = $series;
        $this->creators = $creators;
        $this->meetings = $meetings;
        $this->subjects = $subjects;
        $this->genres = $genres;
        $this->classifications = $classifications;
        $this->notes = $notes;
    }
}


<?php

/*
 * This file is part of the puli/symfony-bundle package.
 *
 * (c) Bernhard Schussek <bschussek@gmail.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Puli\SymfonyBundle\DependencyInjection;

use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Loader;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;

/**
 * @since  1.0
 *
 * @author Bernhard Schussek <bschussek@gmail.com>
 */
class PuliExtension extends Extension
{
    /**
     * {@inheritdoc}
     */
    public function load(array $configs, ContainerBuilder $container)
    {
        $bundles = $container->getParameter('kernel.bundles');

        $configuration = $this->getConfiguration($configs, $container);
        $config = $this->processConfiguration($configuration, $configs);

        $loader = new Loader\XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
        $loader->load('services.xml');

        if (method_exists('Symfony\Component\DependencyInjection\Definition', 'setFactory')) {
            $loader->load('services-2.7.xml');
        } else {
            $loader->load('services-2.6.xml');
        }

        $twigBundleLoaded = isset($bundles['TwigBundle']);
        $twigExtensionLoaded = class_exists('Puli\TwigExtension\PuliExtension');
        $twigEnabled = $config['twig'];

        if ($twigBundleLoaded && $twigExtensionLoaded && $twigEnabled) {
            $loader->load('twig.xml');
        }
    }
}


<?php

namespace Semaphore;

use GuzzleHttp\Client;
use Psr\Http\Message\StreamInterface;

/**
 * Class SemaphoreClient
 * @package Semaphore
 */

class SemaphoreClient
{
	const API_BASE = 'http://beta.semaphore.co/api/v4/';

    public $apikey;
    public $senderName = null;
    protected $client;

	/**
	 * SemaphoreClient constructor.
	 * @param $apikey
	 * @param array $options ( e.g. sendername, apiBase )
	 */
    public function __construct( $apikey, array $options )
    {
        $this->apikey = $apikey;

	    $this->senderName = 'SEMAPHORE';
	    if ( isset( $options['sendername'] ) )
	    {
		    $this->senderName = $options['sendername'];
	    }

	    $apiBase = SemaphoreClient::API_BASE;
	    if ( isset( $options['apiBase'] ) )
        {
            $apiBase = $options['apiBase'];
        }
        $this->client = new Client( ['base_uri' => $apiBase, 'query' => [ 'apikey' => $this->apikey ] ] );
    }

    /**
     * Check the balance of your account
     *
	 * @return StreamInterface
     */
    public function balance()
    {
        $response = $this->client->get( 'account' );
        return $response->getBody();
    }

	/**
	 * Send SMS message(s)
	 *
	 * @param $recipient
	 * @param $message - The message you want to send
	 * @param null $sendername
	 * @return StreamInterface
	 * @throws \Exception
	 * @internal param $number - The recipient phone number(s)
	 * @internal param null $senderId - Optional Sender ID (defaults to initialized value or SEMAPHORE)
	 * @internal param bool|false $bulk - Optional send as bulk
	 */
    public function send( $recipient, $message, $sendername = null )
    {

    	$recipients = explode( ',', $recipient );
    	if( count( $recipients ) > 1000 )
	    {
	    	throw new \Exception( 'API is limited to sending to 1000 recipients at a time' );
	    }

        $params = [
			'form_params' => [
				'apikey' =>  $this->apikey,
				'message' => $message,
				'number' => $recipient,
				'sendername' => $this->senderName
			]
        ];

        if( $sendername != null )
        {
            $params[ 'form_params' ][ 'sendername' ] = $sendername;
        }

        $response = $this->client->post('messages', $params );

        return $response->getBody();
    }

    /**
     * Retrieves data about a specific message
     *
     * @param $messageId - The encoded ID of the message
	 * @return StreamInterface
     */
    public function message( $messageId )
    {
        $params = [
            'query' => [
	            'apikey' =>  $this->apikey,
            ]
        ];
        $response = $this->client->get( 'messages/' . $messageId, $params );
        return $response->getBody();
    }

	/**
	 * Retrieves up to 100 messages, offset by page
	 * @param array $options ( e.g. limit, page, startDate, endDate, status, network, sendername )
	 * @return StreamInterface
	 * @internal param null $page - Optional page for results past the initial 100
	 */
    public function messages( $options )
    {

    	$params = [
            'query' => [
	            'apikey' =>  $this->apikey,
	            'limit' => 100,
                'page' => 1
            ]
        ];

		//Set optional parameters
	    if( array_key_exists( 'limit', $options ) )
	    {
		    $params['query']['limit'] = $options['limit'];
	    }

	    if( array_key_exists( 'page', $options ) )
	    {
		    $params['query']['page'] = $options['page'];
	    }

        if( array_key_exists( 'startDate', $options ) )
        {
        	$params['query']['startDate'] = $options['startDate'];
        }

	    if( array_key_exists( 'endDate', $options ) )
	    {
		    $params['query']['endDate'] = $options['endDate'];
        }

	    if( array_key_exists( 'status', $options ) )
	    {
		    $params['query']['status'] = $options['status'];
        }

	    if( array_key_exists( 'network', $options ) )
	    {
		    $params['query']['network'] = $options['network'];
	    }

	    if( array_key_exists( 'sendername', $options ) )
	    {
		    $params['query']['sendername'] = $options['sendername'];
	    }

	    $response = $this->client->get( 'messages', $params );
        return $response->getBody();
    }

	/**
	 * Get account details
	 *
	 * @return StreamInterface
	 */
	public function account()
	{
		$response = $this->client->get( 'account' );
		return $response->getBody();

	}

	/**
	 * Get users associated with the account
	 *
	 * @return StreamInterface
	 */
	public function users()
	{
		$response = $this->client->get( 'account/users' );
		return $response->getBody();

	}

	/**
	 * Get sender names associated with the account
	 *
	 * @return StreamInterface
	 */
	public function sendernames()
	{
		$response = $this->client->get( 'account/sendernames' );
		return $response->getBody();

	}

	/**
	 * Get transactions associated with the account
	 *
	 * @return StreamInterface
	 */
	public function transactions()
	{
		$response = $this->client->get( 'account/transactions' );
		return $response->getBody();
	}

}


<?php

namespace Scriptotek\SimpleMarcParser;

use Carbon\Carbon;
use Danmichaelo\QuiteSimpleXmlElement\QuiteSimpleXmlElement;

class Record
{
    protected $data;

    public static $subfieldSeparator = '--';

    public function __get($name)
    {
        if (isset($this->data[$name])) {
            return $this->data[$name];
        }

        return;
    }

    public function __set($name, $value)
    {
        if (is_null($value)) {
            unset($this->data[$name]);
        } elseif (is_string($value) && empty($value)) {
            unset($this->data[$name]);
        } else {
            $this->data[$name] = $value;
        }
        // }
    }

    public function __isset($name)
    {
        return isset($this->data[$name]);
    }

    public function __unset($name)
    {
        unset($this->data[$name]);
    }

    /**
     * Convert the object to its JSON representation.
     *
     * @param int $options
     *
     * @return string
     */
    public function toJson($options = 0)
    {
        return json_encode($this->toArray(), $options);
    }

    /**
     * Convert the model instance to an array.
     *
     * @return array
     */
    public function toArray()
    {
        return $this->data;
    }

    /**
     * Parse a *Representation of Dates and Times* (ISO 8601).
     * The date requires 8 numeric characters in the pattern yyyymmdd.
     * The time requires 8 numeric characters in the pattern hhmmss.f,
     * expressed in terms of the 24-hour (00-23) clock.
     *
     * @param string $value
     *
     * @return Carbon|null
     */
    protected function parseDateTime($value)
    {
        if (strlen($value) == 6) {
            return Carbon::createFromFormat('ymdHis', $value . '000000');
        }
        if (strlen($value) == 8) {
            return Carbon::createFromFormat('YmdHis', $value . '000000');
        }
        if (strlen($value) == 16) {
            return Carbon::createFromFormat('YmdHis', substr($value, 0, 14));
        } // skip decimal fraction
    }

    /**
     * Parse a "name node", personal or corporate, main or added, that
     * might have authority information encapsulated.
     *
     * @param string $authority
     * @param array  &$out
     */
    protected function parseAuthority($authority, &$out)
    {
        if (!empty($authority)) {
            $out['id'] = $authority;
            if (preg_match('/\((.*?)\)(.*)/', $authority, $matches)) {
                // As used by at least OCLC and Bibsys
                $out['vocabulary'] = $matches[1];
                $out['id'] = $matches[2];
            }
        }
    }

    /**
     * Parse a "name node", personal or corporate, main or added, that
     * might have relators encapsulated.
     *
     * @param QuiteSimpleXmlElement &$node
     * @param array                 &$out
     * @param string                $default
     */
    protected function parseRelator(&$node, &$out, $default = null)
    {
        $relterm = $node->text('marc:subfield[@code="e"]');
        $relcode = $node->text('marc:subfield[@code="4"]');
        if (!empty($relcode)) {
            $out['role'] = $relcode;
        } elseif (!empty($relterm)) {
            $out['role'] = $relterm;
        } elseif (!is_null($default)) {
            $out['role'] = $default;
        }
    }

    /**
     * Parse a "relationship node", one that have links to other records encapsulated.
     *
     * @param QuiteSimpleXmlElement $node
     *
     * @return array
     */
    protected function parseRelationship($node)
    {
        $rel = array();

        $x = preg_replace('/\(.*?\)/', '', $node->text('marc:subfield[@code="w"]'));
        if (!empty($x)) {
            $rel['id'] = $x;
        }

        $x = $node->text('marc:subfield[@code="t"]');
        if (!empty($x)) {
            $rel['title'] = $x;
        }

        $x = $node->text('marc:subfield[@code="g"]');
        if (!empty($x)) {
            $rel['parts'] = $x;
        }

        $x = $node->text('marc:subfield[@code="x"]');
        if (!empty($x)) {
            $rel['issn'] = $x;
        }

        $x = $node->text('marc:subfield[@code="z"]');
        if (!empty($x)) {
            $rel['isbn'] = $x;
        }

        return $rel;
    }
}


<?php

namespace Rs\Json\Merge;

class Patch
{

    const MEDIA_TYPE = "application/merge-patch+json";

    /**
     * Apply patch on target document.
     *
     * @param  mixed $targetDocument
     * @param  mixed $patchDocument
     * @return mixed
     */
    public function apply($targetDocument, $patchDocument)
    {
        if ($targetDocument === null || !is_object($targetDocument) || is_array($targetDocument)) {
            $targetDocument = new \stdClass();
        }

        if ($patchDocument === null || !is_object($patchDocument) || is_array($patchDocument)) {
            return $patchDocument;
        }

        foreach ($patchDocument as $key => $value) {
            if ($value === null) {
                unset($targetDocument->$key);
            } else {
                if (!isset($targetDocument->$key)) {
                    $targetDocument->$key = null;
                }
                $targetDocument->$key = $this->apply(
                    $targetDocument->$key,
                    $value
                );
            }
        }

        return $targetDocument;
    }

    /**
     * Generate patch for given source document via target document.
     *
     * @param mixed $sourceDocument
     * @param mixed $targetDocument
     *
     * @return mixed
     */
    public function generate($sourceDocument, $targetDocument)
    {
        if ($sourceDocument === null || $targetDocument === null) {
            return $targetDocument;
        }

        if ($sourceDocument == new \stdClass()) {
            return null;
        }

        if (is_array($sourceDocument)) {
            if ($sourceDocument !== $targetDocument) {
                return $targetDocument;
            }

            return null;
        }

        $patchDocument = new \stdClass();
        $sourceDocumentVars = get_object_vars($sourceDocument);
        $targetDocumentVars = get_object_vars($targetDocument);

        foreach ($targetDocumentVars as $var => $value) {
            if (!in_array($var, array_keys($sourceDocumentVars))
                || !in_array($value, array_values($sourceDocumentVars))
            ) {
                $patchDocument->$var = $value;
            }
        }

        foreach ($sourceDocumentVars as $var => $value) {
            if ($targetDocumentVars === []) {
                $patchDocument->$var = null;
                break;
            }

            if (is_object($value)) {
                if ($sourceDocument->$var !== null && is_object($sourceDocument->$var)) {
                    $subPatch = $this->generate($sourceDocument->$var, $targetDocument->$var);
                    if ($subPatch !== null) {
                        $patchDocument->$var = $subPatch;
                    }
                }
            } elseif (!in_array($var, array_keys($targetDocumentVars))
                      || !in_array($value, array_values($targetDocumentVars))) {
                $sourceDocument->$var = null;
                if (!in_array($var, array_keys($targetDocumentVars))) {
                    $patchDocument->$var = null;
                }
            }
        }

        if (count(get_object_vars($patchDocument)) > 0) {
            return $patchDocument;
        }

        return null;
    }

    /**
     * Merge two patch documents.
     *
     * @param mixed $patchDocument1
     * @param mixed $patchDocument2
     *
     * @return mixed
     */
    public function merge($patchDocument1, $patchDocument2)
    {
        if ($patchDocument1 === null || $patchDocument2 === null
            || !is_object($patchDocument1) || !is_object($patchDocument2)
        ) {
            return $patchDocument2;
        }

        $patchDocument = $patchDocument1;
        $patchDocument1Vars = get_object_vars($patchDocument1);
        $patchDocument2Vars = get_object_vars($patchDocument2);

        foreach ($patchDocument2Vars as $var => $value) {
            if (isset($patchDocument1Vars[$var])) {
                $patchDocument->$var = $this->merge(
                  $patchDocument1->$var,
                  $patchDocument2->$var
                );
            } else {
                $patchDocument->$var = $patchDocument2->$var;
            }
        }

        return $patchDocument;
    }
}


<?php
/**
 * This file is part of the JSON Object Mapper package.
 *
 * Copyright 2017 - now by Julian Finkler <julian@mintware.de>
 *
 * For the full copyright and license information, please read the LICENSE
 * file that was distributed with this source code.
 */

namespace MintWare\JOM;

use Doctrine\Common\Annotations\AnnotationReader;
use Doctrine\Common\Annotations\AnnotationRegistry;
use Doctrine\Common\Annotations\DocParser;
use MintWare\JOM\Exception\ClassNotFoundException;
use MintWare\JOM\Exception\InvalidJsonException;
use MintWare\JOM\Exception\PropertyNotAccessibleException;
use MintWare\JOM\Exception\TypeMismatchException;

/**
 * This class is the object mapper
 * To map a json string to a object you can easily call the
 * ObjectMapper::mapJson($json, $targetClass) method.
 *
 * @package MintWare\JOM
 */
class ObjectMapper
{
    /** @var AnnotationReader */
    protected $reader = null;

    private $primitives = [
        'int', 'integer',
        'float', 'double', 'real',
        'bool', 'boolean',
        'array',
        'string',
        'object',
        'date', 'datetime'
    ];

    /**
     * Instantiates a new json object mapper
     * @throws \Exception If the Annotation reader could not be initialized
     */
    public function __construct()
    {
        // Symfony does this also.. ;-)
        AnnotationRegistry::registerLoader('class_exists');

        // Set the annotation reader
        $parser = new DocParser();
        $parser->setIgnoreNotImportedAnnotations(true);
        try {
            $this->reader = new AnnotationReader($parser);
        } catch (\Exception $e) {
            throw new \Exception("Failed to initialize the AnnotationReader", null, $e);
        }
    }

    /**
     * Maps a JSON string to a object
     *
     * @param string $json The JSON string
     * @param string $targetClass The target object class
     *
     * @return mixed The mapped object
     *
     * @throws InvalidJsonException If the JSON is not valid
     * @throws ClassNotFoundException If the target class does not exist
     * @throws PropertyNotAccessibleException If the class property has no public access an no set-Method
     * @throws TypeMismatchException If The type in the JSON does not match the type in the class
     * @throws \ReflectionException If the target class does not exist
     */
    public function mapJson($json, $targetClass)
    {
        // Check if the JSON is valid
        if (!is_array($data = json_decode($json, true))) {
            throw new InvalidJsonException();
        }

        // Pre initialize the result
        $result = null;

        // Check if the target object is a collection of type X
        if (substr($targetClass, -2) == '[]') {
            $result = [];
            foreach ($data as $key => $entryData) {
                // Map the data recursive
                $result[] = $this->mapDataToObject($entryData, substr($targetClass, 0, -2));
            }
        } else {
            // Map the data recursive
            $result = $this->mapDataToObject($data, $targetClass);
        }

        return $result;
    }

    /**
     * Maps the  current entry to the property of the object
     *
     * @param array $data The array of data
     * @param string $targetClass The current object class
     *
     * @return mixed The mapped object
     *
     * @throws ClassNotFoundException If the target class does not exist
     * @throws PropertyNotAccessibleException If the mapped property is not accessible
     * @throws TypeMismatchException If the given type in json does not match with the expected type
     * @throws \ReflectionException If the class does not exist.
     */
    public function mapDataToObject($data, $targetClass)
    {
        $targetClass = preg_replace('~(\\\\){2,}~', '\\', $targetClass);

        // Check if the target object class exists, if not throw an exception
        if (!class_exists($targetClass)) {
            throw new ClassNotFoundException($targetClass);
        }

        // Create the target object
        $object = new $targetClass();

        // Reflecting the target object to extract properties etc.
        $class = new \ReflectionClass($targetClass);

        // Iterate over each class property to check if it's mapped
        foreach ($class->getProperties() as $property) {

            // Extract the Annotations
            $fields = $this->reader->getPropertyAnnotations($property);

            /** @var JsonField $field */
            foreach ($fields as $field) {
                if ($field instanceof JsonField == false) {
                    continue;
                }

                // Check if the property is public accessible or has a setter / adder
                $propertyName = $property->getName();
                $ucw = ucwords($propertyName);
                if (!$property->isPublic() && !($class->hasMethod('set' . $ucw) || $class->hasMethod('add' . $ucw))) {
                    throw new PropertyNotAccessibleException($propertyName);
                }

                if ($field->name == null) {
                    $field->name = $propertyName;
                }

                // Check if the current property is defined in the JSON
                if (!isset($data[$field->name])) continue;
                $val = null;

                $types = explode('|', $field->type);
                $typeKeys = array_keys($types);
                $lastTypeKey = end($typeKeys);

                if ($field->preTransformer !== null) {
                    /** @var TransformerInterface $preTransformer */
                    $preTransformer = $field->preTransformer;
                    $data[$field->name] = $preTransformer::transform($data[$field->name]);
                }

                if ($field->transformer !== null) {
                    /** @var TransformerInterface $transformer */
                    $transformer = $field->transformer;
                    $val = $transformer::transform($data[$field->name]);
                    $types = []; // Ignore type handler!
                }

                foreach ($types as $typeKey => $type) {
                    $isLastElement = ($typeKey == $lastTypeKey);

                    // Check the type of the field and set the val
                    if ($type == '') {
                        $val = $data[$field->name];
                    } elseif (in_array($type, $this->primitives)) {
                        $format = ($field instanceof DateTimeField && $field->format !== null
                            ? $field->format
                            : 'Y-m-d\TH:i:s');

                        $converted = null;
                        try {
                            $converted = $this->castType($data[$field->name], $type, $field->name, $format, true);
                        } catch (TypeMismatchException $ex) {
                            if ($isLastElement) {
                                throw  $ex;
                            }
                            continue;
                        }
                        $val = $converted;
                    } else {
                        // If none of the primitives match it is an custom object

                        // Check if it's an array of X
                        if (substr($type, -2) == '[]' && is_array($data[$field->name])) {
                            $t = substr($type, 0, -2);
                            $val = [];
                            foreach ($data[$field->name] as $entry) {
                                // Map the data recursive
                                $val[] = (object)$this->mapDataToObject($entry, $t);
                            }
                        } elseif (substr($type, -2) != '[]') {
                            // Map the data recursive
                            $val = (object)$this->mapDataToObject($data[$field->name], $type);
                        }
                    }

                    if ($field->postTransformer !== null) {
                        /** @var TransformerInterface $postTransformer */
                        $postTransformer = $field->postTransformer;
                        $val = $postTransformer::transform($val);
                    }

                    if ($val !== null) {
                        break;
                    }
                }

                // Assign the JSON data to the object property
                if ($val !== null) {
                    // If the property is public accessible, set the value directly
                    if ($property->isPublic()) {
                        $object->{$propertyName} = $val;
                    } else {
                        // If not, use the setter / adder
                        $ucw = ucwords($propertyName);
                        if ($class->hasMethod($method = 'set' . $ucw)) {
                            $object->$method($val);
                        } elseif ($class->hasMethod($method = 'add' . $ucw)) {
                            $object->$method($val);
                        }
                    }
                }
            }
        }
        return $object;
    }

    /**
     * Serializes an object to JSON
     *
     * @param object $object The object
     * @param bool $returnAsString For internal usage
     * @return string|array The JSON-String
     *
     * @throws ClassNotFoundException If the target class does not exist
     * @throws PropertyNotAccessibleException If the mapped property is not accessible
     * @throws TypeMismatchException If the given type in json does not match with the expected type
     */
    public function objectToJson($object, $returnAsString = true)
    {
        $jsonData = [];
        // Reflecting the target object to extract properties etc.
        $class = new \ReflectionObject($object);

        // Iterate over each class property to check if it's mapped
        foreach ($class->getProperties() as $property) {
            // Extract the JsonField Annotation

            /** @var JsonField $field */
            $field = $this->reader->getPropertyAnnotation($property, JsonField::class);

            // Is it not defined, the property is not mapped
            if (null === $field) {
                continue;
            }

            // Check if the property is public accessible or has a setter / adder
            $propertyName = $property->getName();
            $ucw = ucwords($propertyName);
            if (!$property->isPublic() && !($class->hasMethod('get' . $ucw))) {
                throw new PropertyNotAccessibleException($propertyName);
            }

            if ($field->name == null) {
                $field->name = $propertyName;
            }

            $val = null;
            if ($property->isPublic()) {
                $val = $object->{$propertyName};
            } else {
                $val = $object->{'get' . $ucw}();
            }

            // Reverse order on encoding (postTransformer -> transformer -> preTransformer)
            if ($field->postTransformer !== null) {
                /** @var TransformerInterface $postTransformer */
                $postTransformer = $field->postTransformer;
                $val = $postTransformer::reverseTransform($val);
            }

            if ($field->transformer !== null) {
                /** @var TransformerInterface $transformer */
                $transformer = $field->transformer;
                $val = $transformer::reverseTransform($val);
            }

            if (is_null($val)) {
                $jsonData[$field->name] = $val;
                continue;
            }

            if ($field->transformer === null) {
                $types = explode('|', $field->type);
                $type = null;

                foreach ($types as $tString) {
                    $type = $tString;
                    if (!is_object($val) || !in_array(strtolower($tString), $this->primitives)) {
                        break;
                    }
                }
                // Check the type of the field and set the val
                if (in_array($type, $this->primitives)) {
                    $format = 'Y-m-d\TH:i:s';
                    if ($field instanceof DateTimeField && $field->format !== null) {
                        $format = $field->format;
                    }
                    $val = $this->castType($val, $type, $propertyName, $format);
                } elseif ($type != null) {
                    // Check if it's an array of X
                    if (substr($type, -2) == '[]' && is_array($val)) {
                        $tmpVal = [];
                        foreach ($val as $entry) {
                            // Map the data recursive
                            $tmpVal[] = (object)$this->objectToJson($entry, false);
                        }
                        $val = $tmpVal;
                    } elseif (substr($type, -2) != '[]') {
                        // Map the data recursive
                        $val = (object)$this->objectToJson($val, false);
                    }
                }
            }

            if ($field->preTransformer !== null) {
                /** @var TransformerInterface $preTransformer */
                $preTransformer = $field->preTransformer;
                $val = $preTransformer::reverseTransform($val);
            }

            // Assign the JSON data to the object property
            if ($val !== null) {
                // If the property is public accessible, set the value directly
                $jsonData[$field->name] = $val;
            }
        }

        $res = $jsonData;
        if ($returnAsString) {
            $res = json_encode($res, JSON_PRETTY_PRINT);
        }

        return $res;
    }

    /**
     * @param mixed $dataToMap The data which should be mapped
     * @param string $type The target type
     * @param string $propertyName The name of the property (required for the exception)
     * @param string $datetimeFormat the format for DateTime deserialization
     * @param bool $fromJson True, if the data comes from the json data
     * @return mixed
     * @throws TypeMismatchException If the data does not match to the type
     *
     * @internal
     */
    private function castType($dataToMap, $type, $propertyName, $datetimeFormat, $fromJson = false)
    {
        $dtCheck = function ($x) {
            return ($x instanceof \DateTime);
        };

        $checkMethod = [
            'int' => 'is_int', 'integer' => 'is_int',
            'float' => 'is_float', 'double' => 'is_float', 'real' => 'is_float',
            'bool' => 'is_bool', 'boolean' => 'is_bool',
            'date' => $dtCheck, 'datetime' => $dtCheck,
            'array' => 'is_array',
            'string' => 'is_string',
            'object' => function ($x) {
                return $x == false ? true : $x;
            },
        ];

        if (!isset($checkMethod[$type])) {
            return null;
        }

        if ($fromJson && in_array($type, ['date', 'datetime'])) {
            // Accepts the following formats:
            // 2017-09-09
            // 2017-09-09 13:20:59
            // 2017-09-09T13:20:59
            // 2017-09-09T13:20:59.511
            // 2017-09-09T13:20:59.511Z
            // 2017-09-09T13:20:59-02:00
            $validPattern = '~^\d{4}-\d{2}-\d{2}((T|\s{1})\d{2}:\d{2}:\d{2}(\.\d{1,3}(Z|)|(\+|\-)\d{2}:\d{2}|)|)$~';

            $tmpVal = $dataToMap;
            if (preg_match($validPattern, $tmpVal)) {
                $dataToMap = new \DateTime($tmpVal);
            } else {
                $casted = intval($tmpVal);
                if (is_numeric($tmpVal) || ($casted == $tmpVal && strlen($casted) == strlen($tmpVal))) {
                    $dataToMap = new \DateTime();
                    $dataToMap->setTimestamp($tmpVal);
                }
            }
        }

        if (!$checkMethod[$type]($dataToMap)) {
            throw new TypeMismatchException($type, gettype($dataToMap), $propertyName);
        }

        if (in_array($type, ['int', 'integer'])) {
            $dataToMap = (int)$dataToMap;
        } elseif (in_array($type, ['float', 'double', 'real'])) {
            $dataToMap = (float)$dataToMap;
        } elseif (in_array($type, ['bool', 'boolean'])) {
            $dataToMap = (bool)$dataToMap;
        } elseif (in_array($type, ['array'])) {
            $dataToMap = (array)$dataToMap;
        } elseif (in_array($type, ['string'])) {
            $dataToMap = (string)$dataToMap;
        } elseif (in_array($type, ['object'])) {
            $tmpVal = $dataToMap;
            if (is_array($tmpVal) && array_keys($tmpVal) != range(0, count($tmpVal))) {
                $dataToMap = (object)$tmpVal;
            }
            if (!is_object($dataToMap)) {
                throw new TypeMismatchException($type, gettype($dataToMap), $propertyName);
            }
            $dataToMap = (object)$dataToMap;
        } elseif (in_array($type, ['date', 'datetime'])) {
            if ($fromJson) {
                if (strtolower($type) == 'date') {
                    $dataToMap->setTime(0, 0, 0);
                }
            } else {

                /** @var \DateTime $dataToMap */
                if (strtolower($datetimeFormat) !== 'timestamp') {
                    $dataToMap = $dataToMap->format($datetimeFormat);
                } else {
                    $dataToMap = $dataToMap->getTimestamp();
                }
            }
        }
        return $dataToMap;
    }
}


<?php
namespace SlimApi\Command;

use \Exception;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Slim\App;

class RoutesCommand extends Command
{
    /**
     * {@inheritdoc}
     */
    protected function configure()
    {
        $this
            ->setName('routes')
            ->setDescription('Retrieves routes for app.')
        ;
    }

    /**
     * {@inheritdoc}
     */
    protected function execute(InputInterface $input, OutputInterface $output)
    {
        if (false === is_file('composer.json')) {
            throw new Exception('Commands must be run from root of project.');
        }

        try {
            $app  = new App();
            require 'src/routes.php';
            $routes = $app->getContainer()->get('router')->getRoutes();
            foreach ($routes as $route) {
                foreach ($route->getMethods() as $method) {
                    $name     = str_pad($route->getName(), 10, ' ', STR_PAD_LEFT);
                    $method   = str_pad($method, 8, ' ', STR_PAD_RIGHT);
                    $routeStr = str_pad($route->getPattern(), 15, ' ', STR_PAD_RIGHT);
                    $resolvesTo = (is_string($route->getCallable()) ? $route->getCallable() : '');
                    $output->writeln('<info>'.$name.$method.$routeStr.$resolvesTo.'</info>');
                }
            }
        } catch (Exception $e) {
            $output->writeln($e->getMessage());
        }
    }
}


<?php
namespace SlimApi\Service;

class ConfigService
{
    /**
     * Fetches configuration from the running app
     *
     * @return array
     */
    public static function fetch($dir = false)
    {
        if (false === $dir) {
            $dir = getcwd();
        }
        $config = [];
        $files  = glob($dir.'/config/*.config.php', GLOB_BRACE);
        foreach ($files as $file) {
            $config = array_merge($config, (require $file));
        }
        return $config;
    }
}


<?php
namespace SlimApi\Command;

use \Exception;
use SlimApi\Skeleton\SkeletonInterface;
use SlimApi\Migration\MigrationInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;

class InitCommand extends Command
{
    public static $successMessage = "Application correctly created. Don't forget to run composer install!";
    public static $failureMessage = "Application failed to create.";

    /**
     * {@inheritdoc}
     */
    public function __construct(SkeletonInterface $skeletonService, MigrationInterface $migrationService)
    {
        parent::__construct();
        $this->skeletonService = $skeletonService;
        $this->migrationService = $migrationService;
    }

    /**
     * {@inheritdoc}
     */
    protected function configure()
    {
        $this
            ->setName('init')
            ->setDescription('Creates a default slim-api application.')
            ->addArgument(
                'name',
                InputArgument::REQUIRED,
                'Application name (becomes namespace).'
            )
            ->addArgument(
                'location',
                InputArgument::OPTIONAL,
                'Where do you want to create the application?',
                getcwd()
            )
        ;
    }

    /**
     * {@inheritdoc}
     */
    protected function execute(InputInterface $input, OutputInterface $output)
    {
        $name    = ucfirst($input->getArgument('name'));
        $pattern = '/^[A-Z][a-zA-Z0-9]*$/';
        if (1 !== preg_match($pattern, $name)) {
            throw new Exception('Invalid name.');
        }

        $path       = $input->getArgument('location');
        $path       = ('/' === $path[0]) ? $path : getcwd().'/'.$path;
        $pathExists = is_dir($path);
        $pathInfo   = pathinfo($path);
        if (
            ($pathExists && !is_writable($path)) ||
            (!$pathExists && !is_writable($pathInfo['dirname']))
        ) {
            throw new Exception('Cannot write to path.');
        }

        if ($pathExists && 2 !== count(scandir($path))) {
            throw new Exception('Path not empty.');
        }

        try {
            $this->skeletonService->create($path, $name);
            $this->migrationService->init($path);
            $output->writeln('<info>'.static::$successMessage.'</info>');
        } catch (Exception $e) {
            $output->writeln($e->getMessage());
        }
    }
}


<?php
namespace SlimApi\Command;

use \Exception;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;

class GenerateCommand extends Command
{
    /**
     * @param mixed $generatorFactory
     *
     * @return
     */
    public function __construct($generatorFactory)
    {
        parent::__construct();
        $this->generatorFactory = $generatorFactory;
    }

    /**
     * {@inheritdoc}
     */
    protected function configure()
    {
        $this
            ->setName('generate')
            ->setAliases(['g'])
            ->setDescription('Creates a default slim-api application.')
            ->addArgument(
                'type',
                InputArgument::REQUIRED,
                'What type of generator? [scaffold, controller, model].'
            )
            ->addArgument(
                'name',
                InputArgument::REQUIRED,
                'Resultant resource name.'
            )
            ->addArgument(
                'fields',
                InputArgument::IS_ARRAY,
                'What fields (if appropriate).'
            )
            ->addOption(
                'api-version',
                null,
                InputOption::VALUE_OPTIONAL,
                "When creating scaffold/controller/route this surrounds the route in a version"
            )
            ->setHelp(<<<EOT
The <info>generate</info> command help to generate controllers/models/routes or all together as scaffold.
Model/scaffold generators take the db definition as extra arguments, in the form name:type:length:nullable:unique in most cases.
<info>slimapi generate scaffold user oauthToken:text::false:true token:string:128:false:true role:string:128:false</info>
In the case of the type being reference the name should be the reference, and it also takes extra arguments for action when foreign key deleted and updated.
<info>slimapi generate scaffold scaffold foo field1:integer field2:string user:reference</info>
<info>slimapi generate scaffold scaffold bar field1:integer field2:string user:reference:::NO_ACTION:CASCADE</info>
EOT
            )
        ;
    }

    /**
     * {@inheritdoc}
     */
    protected function execute(InputInterface $input, OutputInterface $output)
    {
        $type = $input->getArgument('type');
        if (!in_array($type, ['scaffold', 'controller', 'model', 'migration'])) {
            throw new Exception('Invalid type.');
        }

        $name    = ucfirst($input->getArgument('name'));
        $pattern = '/^[A-Z][a-zA-Z0-9]*$/';
        if (1 !== preg_match($pattern, $name)) {
            throw new Exception('Invalid name.');
        }

        $fields = $input->getArgument('fields');
        if (!is_array($fields)) {
            throw new Exception('Invalid fields.');
        }

        if (false === is_file('composer.json')) {
            throw new Exception('Commands must be run from root of project.');
        }

        $version = $input->getOption('api-version');

        $generator = $this->generatorFactory->fetch($type);
        if (!$generator->validate($name, $fields)) {
            throw new Exception('Fields not valid.');
        }

        try {
            $generator->process($name, $fields, ['version' => $version]);
            $output->writeln('<info>Generation completed.</info>');
        } catch (Exception $e) {
            echo $e->getMessage();
        }

        return true;
    }
}


<?php
namespace SlimApi\Factory;

use SlimApi\Generator\GeneratorInterface;

class GeneratorFactory
{
    private $generators = [];

    /**
     * Fetches appropriate generator
     *
     * @param string $name
     *
     * @return GeneratorInterface|false the required generator or false if none.
     */
    public function fetch($name)
    {
        $generator = false;
        if (array_key_exists($name, $this->generators)) {
            $generator = $this->generators[$name];
        }
        return $generator;
    }

    /**
     * Add a generator to the factory
     *
     * @param string               $name   the name of the Generator
     * @param GeneratorInterface   $class  the generator object to return for the specified name
     */
    public function add($name, GeneratorInterface $class)
    {
        if (array_key_exists($name, $this->generators)) {
            throw new \InvalidArgumentException('Generator already exists.');
        }

        $this->generators[$name] = $class;
    }
}


<?php

namespace MarvinLabs\Html\FontAwesome;

use MarvinLabs\Html\FontAwesome\Elements\FontAwesomeIcon;
use Spatie\Html\Html;

/**
 * Class FontAwesome
 * @package MarvinLabs\Html\FontAwesome
 *
 *          Extend Spatie's HTML builder in order to automatically inject what Bootstrap likes and some other libraries
 *          too
 */
class FontAwesome
{

    /** @var \Spatie\Html\Html */
    protected $html;

    /**
     * FontAwesome constructor.
     *
     * @param \Spatie\Html\Html $html
     */
    public function __construct(Html $html)
    {
        $this->html = $html;
    }

    /**
     * Link to the latest font-awesome CSS served from maxcdn.bootstrapcdn.com
     *
     * @return \Spatie\Html\Elements\Element
     */
    public function css()
    {
        /** @var \MarvinLabs\Html\FontAwesome\FontAwesome $this */
        return $this->html->element('link')->attributes([
            'href' => '//maxcdn.bootstrapcdn.com/font-awesome/latest/css/font-awesome.min.css',
            'rel'  => 'stylesheet',
        ]);
    }

    /**
     * Insert a FontAwesome icon
     *
     * @param string $name Name of the icon (without any 'fa-' prefix)
     *
     * @return \MarvinLabs\Html\FontAwesome\Elements\FontAwesomeIcon
     */
    public function icon($name)
    {
        return FontAwesomeIcon::create()->name($name);
    }

    /**
     * Stack two icons on top of each other. The array must contain exactly 2 elements.
     *
     * An element of the array can either be:
     *
     * - A simple string, in which case it is considered to be the icon name
     * - A key/value pair, both being strings, in which case the key is the icon name and the value will be added to
     *   the icon css classes
     * - A FontAwesomeIcon object, obtained using the icon function.
     *
     * @param array $icons The 2 icons to stack. First is back, second is front.
     *
     * @return \Spatie\Html\Elements\Span
     *
     * @throws \InvalidArgumentException
     */
    public function stack($icons)
    {
        if (count($icons) !== 2)
        {
            throw new \InvalidArgumentException('Expecting exactly 2 icons in the stack');
        }

        $contents = [];
        $index = 2;
        foreach ($icons as $key => $value)
        {
            $contents[] = $this->getStackIconElement($key, $value, $index);
            --$index;
        }

        return $this->html->span($contents)->addClass('fa-stack');
    }

    /**
     * Magic method which will take care of calling the `icon` method when given an icon name directly.
     *
     * For instance `fa()->beer()` will be caught here and turned into `fa()->icon('beer')`
     *
     * @param string $name
     * @param array  $arguments
     *
     * @return \MarvinLabs\Html\FontAwesome\Elements\FontAwesomeIcon
     */
    public function __call($name, $arguments)
    {
        if (empty($arguments))
        {
            return $this->icon(kebab_case($name));
        }
    }

    /**
     * Sanitize an element passed to the FA stack
     *
     * @param int|string                                                   $key
     * @param string|\MarvinLabs\Html\FontAwesome\Elements\FontAwesomeIcon $value
     * @param int                                                          $index Starting at 1
     *
     * @return \MarvinLabs\Html\FontAwesome\Elements\FontAwesomeIcon
     * @throws \InvalidArgumentException
     */
    protected function getStackIconElement($key, $value, $index)
    {
        $element = $value;

        if (is_string($key))
        {
            $element = $this->icon($key)->addClass($value);
        }
        else if (is_string($value))
        {
            $element = $this->icon($value);
        }

        if ( !is_a($element, FontAwesomeIcon::class))
        {
            throw new \InvalidArgumentException('Invalid icon passed to stack');
        }

        return $element->addClass("fa-stack-{$index}x");
    }
}


<?php
namespace SlimApi\Skeleton;

class SkeletonService implements SkeletonInterface
{
    /**
     * {@inheritdoc}
     */
    public function __construct($structure)
    {
        $this->structure = $structure;
    }

    /**
     * {@inheritdoc}
     */
    public function create($path, $name, $structure = false)
    {
        if (false === $structure) {
            $structure = $this->structure;
        }

        // should only happen the first time
        if ( ! is_dir($path)) {
            mkdir($path, 0777, true);
        }

    	foreach ($structure as $folder => $subFolder) {
    		// Folder with subfolders
    		if (is_array($subFolder)) {
    			$newPath = "{$path}/{$folder}";
    			if ( ! is_dir($newPath)) {
                    mkdir($newPath);
                }
    			$this->create($newPath, $name, $subFolder);
    		} else {
                // filename with content
                $content  = strtr($subFolder, ['$name' => $name]);
    			$filePath = "{$path}/{$folder}";
    			if ( ! is_file($filePath)) {
                    file_put_contents($filePath, $content);
                }
    		}
    	}
    }
}


<?php
namespace SlimApi\Service;

use SlimApi\Interfaces\GeneratorServiceInterface;

class RouteService implements GeneratorServiceInterface
{
    public $commands = [];

    /**
     * @param string $routerFileLocation
     * @param string $routeTemplate
     * @param string $namespaceRoot
     *
     * @return
     */
    public function __construct($routerFileLocation, $routeTemplate, $namespaceRoot)
    {
        $this->routerFileLocation = $routerFileLocation;
        $this->template           = $routeTemplate;
        $this->controllerLocation = $namespaceRoot.'\Controller\$nameController';
    }

    /**
     * {@inheritdoc}
     */
    public function processCommand($type, ...$arguments)
    {
        // $arguments is method1, method2 etc
        array_shift($arguments);
        switch ($type) {
            case 'addRoute':
                foreach ($arguments as $method) {
                    $this->addRoute($method);
                }
                break;
            default:
                throw new \Exception('Invalid route command.');
                break;
        }
    }

    /**
     * {@inheritdoc}
     */
    public function create($name, $options = [])
    {
        $version = '/'.(array_key_exists('version', $options) ? $options['version'] : '');
        $content = PHP_EOL.PHP_EOL.implode(PHP_EOL, $this->commands);
        $content = strtr($content, ['$route' => '/'.strtolower($name), '$name' => $name, '$version' => $version]);
        return file_put_contents($this->targetLocation($name), $content, FILE_APPEND);
    }

    /**
     * {@inheritdoc}
     */
    public function targetLocation($name)
    {
        return $this->routerFileLocation;
    }

    /**
     * Process the requrested method into a route template
     *
     * @param string $method
     *
     * @return void
     */
    private function addRoute($method)
    {
        switch ($method) {
            case 'index':
                $methodMap          = ['GET'];
                $realRoute          = '$route';
                $controllerCallable = $this->controllerLocation.':indexAction';
                break;
            case 'get':
                $methodMap          = ['GET'];
                $realRoute          = '$route/{id}';
                $controllerCallable = $this->controllerLocation.':getAction';
                break;
            case 'post':
                $methodMap          = ['POST'];
                $realRoute          = '$route';
                $controllerCallable = $this->controllerLocation.':postAction';
                break;
            case 'put':
                $methodMap          = ['POST', 'PUT'];
                $realRoute          = '$route/{id}';
                $controllerCallable = $this->controllerLocation.':putAction';
                break;
            case 'delete':
                $methodMap          = ['DELETE'];
                $realRoute          = '$route/{id}';
                $controllerCallable = $this->controllerLocation.':deleteAction';
                break;
            default:
                throw new \Exception('Invalid method.'.$method);
                break;
        }
        $methodMap = "['".implode("', '", $methodMap)."']";
        $command   = strtr($this->template, ['$methodMap' => $methodMap, '$route' => $realRoute, '$controllerCallable' => $controllerCallable]);
        // $app->map(['GET'], '/trails', 'EarlyBird\Controllers\TrailsController:index');
        // $app->map(['GET'], '/texts/{stage}', 'EarlyBird\Controllers\TrailsController:get');
        // $app->map(['POST'], '/trails', 'EarlyBird\Controllers\TrailsController:post');
        // $app->map(['POST', 'PUT'], '/trails/{id}', 'EarlyBird\Controllers\TrailsController:put');
        $this->commands[] = $command;
    }
}


<?php
namespace SlimApi;

use SlimApi\Service\ConfigService;
use SlimApi\Service\ModuleService;

class Module
{
    /**
     * load the dependencies for the module.
     */
    public function loadDependencies()
    {
        // load the apis config
        $config = ConfigService::fetch(dirname(__DIR__));
        // load the app config
        $config = array_merge($config, ConfigService::fetch());

        $moduleService = new ModuleService;
        if (! array_key_exists('slim-api', $config)) {
            $config['slim-api'] = [
                'modules' => [
                    'SlimApi\Phinx', //provides migrations
                    'SlimApi\Mvc' //provides structure
                ]
            ];
        } else {
            // load the target autoloader
            require 'vendor/autoload.php';
        }

        foreach ($config['slim-api']['modules'] as $moduleNamespace) {
            $config = array_merge($config, $moduleService->load($moduleNamespace));
        }

        return $config;
    }
}