Completed
Pull Request — master (#114)
by Bart
02:01
created

ModelProcessor::import()   D

Complexity

Conditions 9
Paths 12

Size

Total Lines 41
Code Lines 28

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 41
rs 4.909
c 0
b 0
f 0
cc 9
eloc 28
nc 12
nop 4
1
<?php
2
3
namespace NerdsAndCompany\Schematic\Services;
4
5
use Craft;
6
use craft\base\Model;
7
use craft\helpers\ArrayHelper;
8
use NerdsAndCompany\Schematic\Schematic;
9
use NerdsAndCompany\Schematic\Converters\Base as BaseConverter;
10
use NerdsAndCompany\Schematic\Interfaces\MappingInterface;
11
use yii\base\Component as BaseComponent;
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): array
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): array
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
            $record = $this->findOrNewRecord($recordsByHandle, $definition, $handle);
67
68
            if ($converter->getRecordDefinition($record) === $definition) {
69
                Schematic::info('- Skipping '.get_class($record).' '.$handle);
70
            } else {
71
                $converter->setRecordAttributes($record, $definition, $defaultAttributes);
72
                if ($persist) {
73
                    Schematic::info('- Saving '.get_class($record).' '.$handle);
74
                    if ($converter->saveRecord($record, $definition)) {
0 ignored issues
show
Unused Code introduced by
This if statement is empty and can be removed.

This check looks for the bodies of if statements that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

These if bodies can be removed. If you have an empty if but statements in the else branch, consider inverting the condition.

if (rand(1, 6) > 3) {
//print "Check failed";
} else {
    print "Check succeeded";
}

could be turned into

if (rand(1, 6) <= 3) {
    print "Check succeeded";
}

This is much more concise to read.

Loading history...
75
                    } else {
76
                        Schematic::importError($record, $handle);
77
                    }
78
                }
79
            }
80
            $imported[] = $record;
81
            unset($recordsByHandle[$handle]);
82
        }
83
84
        if (Schematic::$force && $persist) {
85
            // Delete records not in definitions
86
            foreach ($recordsByHandle as $handle => $record) {
87
                $modelClass = get_class($record);
88
                Schematic::info('- Deleting '.get_class($record).' '.$handle);
89
                $converter = $this->getConverter($modelClass);
90
                $converter->deleteRecord($record);
91
            }
92
        }
93
94
        return $imported;
95
    }
96
97
    /**
98
     * Find record from records by handle or new record.
99
     *
100
     * @param Model[] $recordsByHandle
101
     * @param array   $definition
102
     * @param string  $handle
103
     *
104
     * @return Model
105
     */
106
    private function findOrNewRecord(array $recordsByHandle, array $definition, string $handle): Model
107
    {
108
        $record = new  $definition['class']();
109
        if (array_key_exists($handle, $recordsByHandle)) {
110
            $existing = $recordsByHandle[$handle];
111
            if (get_class($record) == get_class($existing)) {
112
                $record = $existing;
113
            } else {
114
                $record->id = $existing->id;
115
                $record->setAttributes($existing->getAttributes());
116
            }
117
        }
118
119
        return $record;
120
    }
121
122
    /**
123
     * Find converter class for model.
124
     *
125
     * @param string $modelClass
126
     *
127
     * @return BaseConverter
128
     */
129
    protected function getConverter(string $modelClass)
130
    {
131
        if ($modelClass) {
132
            $converterClass = 'NerdsAndCompany\\Schematic\\Converters\\'.ucfirst(str_replace('craft\\', '', $modelClass));
133
            if (class_exists($converterClass)) {
134
                return new $converterClass();
135
            }
136
137
            return $this->getConverter(get_parent_class($modelClass));
138
        }
139
140
        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...
141
    }
142
}
143