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

ModelMapper::export()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 15
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

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