Completed
Pull Request — master (#114)
by Bart
10:19 queued 21s
created

ModelProcessor::getConverter()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 13
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 13
rs 9.4285
cc 3
eloc 7
nc 3
nop 1
1
<?php
2
3
namespace NerdsAndCompany\Schematic\Services;
4
5
use Craft;
6
use craft\base\Model;
7
use craft\helpers\ArrayHelper;
8
use yii\base\Component as BaseComponent;
9
use NerdsAndCompany\Schematic\Interfaces\MappingInterface;
10
use NerdsAndCompany\Schematic\Schematic;
11
use NerdsAndCompany\Schematic\Converters\Base as BaseConverter;
12
13
/**
14
 * Schematic Base Service.
15
 *
16
 * Sync Craft Setups.
17
 *
18
 * @author    Nerds & Company
19
 * @copyright Copyright (c) 2015-2018, Nerds & Company
20
 * @license   MIT
21
 *
22
 * @see      http://www.nerds.company
23
 */
24
class ModelProcessor extends BaseComponent implements MappingInterface
25
{
26
    /**
27
     * Get all record definitions.
28
     *
29
     * @return array
30
     */
31
    public function export(array $records = [])
32
    {
33
        $result = [];
34
        foreach ($records as $record) {
35
            $modelClass = get_class($record);
36
            $converter = $this->getConverter($modelClass);
37
            if ($converter == false) {
38
                Schematic::error('No converter found for '.$modelClass);
39
                continue;
40
            }
41
            $result[$record->handle] = $converter->getRecordDefinition($record);
42
        }
43
44
        return $result;
45
    }
46
47
    /**
48
     * Import records.
49
     *
50
     * @param array $definitions
51
     * @param Model $records           The existing records
52
     * @param array $defaultAttributes Default attributes to use for each record
53
     * @param bool  $persist           Whether to persist the parsed records
54
     */
55
    public function import(array $definitions, array $records = [], array $defaultAttributes = [], $persist = true)
56
    {
57
        $imported = [];
58
        $recordsByHandle = ArrayHelper::index($records, 'handle');
59
        foreach ($definitions as $handle => $definition) {
60
            $modelClass = $definition['class'];
61
            $converter = $this->getConverter($modelClass);
62
            if ($converter == false) {
63
                Schematic::error('No converter found for '.$modelClass);
64
                continue;
65
            }
66
67
            $record = new $modelClass();
68
            if (array_key_exists($handle, $recordsByHandle)) {
69
                $existing = $recordsByHandle[$handle];
70
                if (get_class($record) == get_class($existing)) {
71
                    $record = $existing;
72
                } else {
73
                    $record->id = $existing->id;
74
                    $record->setAttributes($existing->getAttributes());
75
                }
76
77
                if ($converter->getRecordDefinition($record) === $definition) {
78
                    Schematic::info('- Skipping '.get_class($record).' '.$handle);
79
                    unset($recordsByHandle[$handle]);
80
                    continue;
81
                }
82
            }
83
84
            Schematic::info('- Saving '.get_class($record).' '.$handle);
85
            $converter->setRecordAttributes($record, $definition, $defaultAttributes);
86
            if (!$persist || $converter->saveRecord($record, $definition)) {
87
                $imported[] = $record;
88
            } else {
89
                $this->importError($record, $handle);
90
            }
91
            unset($recordsByHandle[$handle]);
92
        }
93
94
        if (Schematic::$force && $persist) {
95
            // Delete records not in definitions
96
            foreach ($recordsByHandle as $handle => $record) {
97
                $modelClass = get_class($record);
98
                Schematic::info('- Deleting '.get_class($record).' '.$handle);
99
                $converter = $this->getConverter($modelClass);
100
                $converter->deleteRecord($record);
101
            }
102
        }
103
104
        return $imported;
105
    }
106
107
    /**
108
     * Find converter class for model.
109
     *
110
     * @param string $modelClass
111
     *
112
     * @return BaseConverter
113
     */
114
    protected function getConverter(string $modelClass)
115
    {
116
        if ($modelClass) {
117
            $converterClass = 'NerdsAndCompany\\Schematic\\Converters\\'.ucfirst(str_replace('craft\\', '', $modelClass));
118
            if (class_exists($converterClass)) {
119
                return new $converterClass();
120
            }
121
122
            return $this->getConverter(get_parent_class($modelClass));
123
        }
124
125
        return false;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return false; (false) is incompatible with the return type documented by NerdsAndCompany\Schemati...Processor::getConverter of type NerdsAndCompany\Schematic\Converters\Base.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
126
    }
127
128
    /**
129
     * Log an import error.
130
     *
131
     * @param Model  $record
132
     * @param string $handle
133
     */
134
    public function importError(Model $record, string $handle)
135
    {
136
        Schematic::warning('- Error importing '.get_class($record).' '.$handle);
137
        foreach ($record->getErrors() as $errors) {
138
            foreach ($errors as $error) {
139
                Schematic::error('   - '.$error);
140
            }
141
        }
142
    }
143
}
144