Completed
Pull Request — master (#114)
by Bart
08:57
created

ModelProcessor::import()   C

Complexity

Conditions 9
Paths 30

Size

Total Lines 46
Code Lines 31

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 46
rs 5.0942
cc 9
eloc 31
nc 30
nop 3
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
     */
54
    public function import(array $definitions, array $records = [], array $defaultAttributes = [])
55
    {
56
        $recordsByHandle = ArrayHelper::index($records, 'handle');
57
        foreach ($definitions as $handle => $definition) {
58
            $modelClass = $definition['class'];
59
            $converter = $this->getConverter($modelClass);
60
            if ($converter == false) {
61
                Schematic::error('No converter found for '.$modelClass);
62
                continue;
63
            }
64
65
            $record = new $modelClass();
66
            if (array_key_exists($handle, $recordsByHandle)) {
67
                $existing = $recordsByHandle[$handle];
68
                if (get_class($record) == get_class($existing)) {
69
                    $record = $existing;
70
                } else {
71
                    $record->id = $existing->id;
72
                    $record->setAttributes($existing->getAttributes());
73
                }
74
75
                if ($converter->getRecordDefinition($record) === $definition) {
76
                    Schematic::info('- Skipping '.get_class($record).' '.$handle);
77
                    unset($recordsByHandle[$handle]);
78
                    continue;
79
                }
80
            }
81
82
            Schematic::info('- Saving '.get_class($record).' '.$handle);
83
            $converter->setRecordAttributes($record, $definition, $defaultAttributes);
84
            if (!$converter->saveRecord($record, $definition)) {
85
                $this->importError($record, $handle);
86
            }
87
            unset($recordsByHandle[$handle]);
88
        }
89
90
        if (Schematic::$force) {
91
            // Delete records not in definitions
92
            foreach ($recordsByHandle as $handle => $record) {
93
                $modelClass = get_class($record);
94
                Schematic::info('- Deleting '.get_class($record).' '.$handle);
95
                $converter = $this->getConverter($modelClass);
96
                $converter->deleteRecord($record);
97
            }
98
        }
99
    }
100
101
    /**
102
     * Find converter class for model.
103
     *
104
     * @param string $modelClass
105
     *
106
     * @return BaseConverter
107
     */
108
    protected function getConverter(string $modelClass)
109
    {
110
        if ($modelClass) {
111
            $converterClass = 'NerdsAndCompany\\Schematic\\Converters\\'.ucfirst(ltrim($modelClass, 'craft\\'));
112
            if (class_exists($converterClass)) {
113
                return new $converterClass();
114
            }
115
116
            return $this->getConverter(get_parent_class($modelClass));
117
        }
118
119
        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...
120
    }
121
122
    /**
123
     * Log an import error.
124
     *
125
     * @param Model  $record
126
     * @param string $handle
127
     */
128
    protected function importError(Model $record, string $handle)
129
    {
130
        Schematic::warning('- Error importing '.get_class($record).' '.$handle);
131
        foreach ($record->getErrors() as $errors) {
132
            foreach ($errors as $error) {
133
                Schematic::error('   - '.$error);
134
            }
135
        }
136
    }
137
}
138