Completed
Pull Request — master (#114)
by Bart
16:27 queued 06:28
created

Sections::populateSectionLocales()   B

Complexity

Conditions 3
Paths 3

Size

Total Lines 25
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 0
Metric Value
dl 0
loc 25
ccs 0
cts 16
cp 0
rs 8.8571
c 0
b 0
f 0
cc 3
eloc 13
nc 3
nop 2
crap 12
1
<?php
2
3
namespace NerdsAndCompany\Schematic\Services;
4
5
use Craft;
6
use craft\base\Model;
7
use craft\models\Section;
8
use craft\models\EntryType;
9
10
/**
11
 * Schematic Sections.
12
 *
13
 * Sync Craft Setups.
14
 *
15
 * @author    Nerds & Company
16
 * @copyright Copyright (c) 2015-2018, Nerds & Company
17
 * @license   MIT
18
 *
19
 * @see      http://www.nerds.company
20
 */
21
class Sections extends Base
22
{
23
    /**
24
     * Get all section records
25
     *
26
     * @return Section[]
27
     */
28
    protected function getRecords()
29
    {
30
        return Craft::$app->sections->getAllSections();
31
    }
32
33
    /**
34
     * Get section definition.
35
     *
36
     * @param Model $record
37
     *
38
     * @return array
39
     */
40
    protected function getRecordDefinition(Model $record)
41
    {
42
        $attributes = parent::getRecordDefinition($record);
43
        if ($record instanceof Section) {
44
            $attributes['entryTypes'] = $this->export($record->getEntryTypes());
45
        }
46
        if ($record instanceof EntryType) {
47
            unset($attributes['sectionId']);
48
        }
49
50
        return $attributes;
51
    }
52
53
    //==============================================================================================================
54
    //================================================  IMPORT  ====================================================
55
    //==============================================================================================================
56
57
    /**
58
     * Attempt to import sections.
59
     *
60
     * @param array $sectionDefinitions
61
     * @param bool  $force              If set to true sections not included in the import will be deleted
62
     *
63
     * @return Result
64
     */
65
    public function import($force = false, array $sectionDefinitions = null)
66
    {
67
        Craft::info(Craft::t('Importing Sections', 'schematic'));
68
69
        $sections = Craft::$app->sections->getAllSections('handle');
70
71
        foreach ($sectionDefinitions as $sectionHandle => $sectionDefinition) {
0 ignored issues
show
Bug introduced by
The expression $sectionDefinitions of type null|array is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
72
            $section = array_key_exists($sectionHandle, $sections)
73
                ? $sections[$sectionHandle]
74
                : new Section();
75
76
            unset($sections[$sectionHandle]);
77
78
            if ($sectionDefinition === $this->getSectionDefinition($section, null)) {
0 ignored issues
show
Documentation Bug introduced by
The method getSectionDefinition does not exist on object<NerdsAndCompany\S...atic\Services\Sections>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
79
                Craft::info(Craft::t('Skipping `{name}`, no changes detected', ['name' => $section->name], 'schematic'));
0 ignored issues
show
Documentation introduced by
array('name' => $section->name) is of type array<string,?,{"name":"?"}>, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
Documentation introduced by
'schematic' is of type string, but the function expects a array.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
80
                continue;
81
            }
82
83
            if (!array_key_exists('locales', $sectionDefinition)) {
84
                $this->addError('`sections[handle].locales` must be defined');
0 ignored issues
show
Documentation Bug introduced by
The method addError does not exist on object<NerdsAndCompany\S...atic\Services\Sections>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
85
86
                continue;
87
            }
88
89
            if (!array_key_exists('entryTypes', $sectionDefinition)) {
90
                $this->addError('errors', '`sections[handle].entryTypes` must exist be defined');
0 ignored issues
show
Documentation Bug introduced by
The method addError does not exist on object<NerdsAndCompany\S...atic\Services\Sections>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
91
92
                continue;
93
            }
94
95
            Craft::info(Craft::t('Importing section `{name}`', ['name' => $sectionDefinition['name']], 'schematic'));
0 ignored issues
show
Documentation introduced by
array('name' => $sectionDefinition['name']) is of type array<string,?,{"name":"?"}>, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
Documentation introduced by
'schematic' is of type string, but the function expects a array.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
96
97
            $this->populateSection($section, $sectionDefinition, $sectionHandle);
98
99
            // Create initial section record
100
            if (!$this->preSaveSection($section)) {
101
                $this->addErrors($section->getAllErrors());
0 ignored issues
show
Documentation Bug introduced by
The method addErrors does not exist on object<NerdsAndCompany\S...atic\Services\Sections>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
102
103
                continue;
104
            }
105
106
            $this->importEntryTypes($section, $sectionDefinition['entryTypes'], $force);
107
108
            // Save section via craft after entrytypes have been created
109
            if (!Craft::$app->sections->saveSection($section)) {
110
                $this->addErrors($section->getAllErrors());
0 ignored issues
show
Documentation Bug introduced by
The method addErrors does not exist on object<NerdsAndCompany\S...atic\Services\Sections>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
111
            }
112
        }
113
114
        if ($force) {
115
            foreach ($sections as $section) {
116
                Craft::$app->sections->deleteSectionById($section->id);
117
            }
118
        }
119
120
        return $this->getResultModel();
0 ignored issues
show
Documentation Bug introduced by
The method getResultModel does not exist on object<NerdsAndCompany\S...atic\Services\Sections>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
121
    }
122
123
    /**
124
     * @param Section $section
125
     * @param array   $entryTypeDefinitions
126
     * @param bool    $force
127
     */
128
    private function importEntryTypes(Section $section, array $entryTypeDefinitions, $force)
129
    {
130
        $entryTypes = Craft::$app->sections->getEntryTypesBySectionId($section->id, 'handle');
131
132
        foreach ($entryTypeDefinitions as $entryTypeHandle => $entryTypeDefinition) {
133
            $entryType = array_key_exists($entryTypeHandle, $entryTypes)
134
                ? $entryTypes[$entryTypeHandle]
135
                : new EntryTypeModel();
136
137
            unset($entryTypes[$entryTypeHandle]);
138
139
            $this->populateEntryType($entryType, $entryTypeDefinition, $entryTypeHandle, $section->id);
140
141
            if (!Craft::$app->sections->saveEntryType($entryType)) {
142
                $this->addError($entryType->getAllErrors());
0 ignored issues
show
Documentation Bug introduced by
The method addError does not exist on object<NerdsAndCompany\S...atic\Services\Sections>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
143
144
                continue;
145
            }
146
        }
147
148
        if ($force) {
149
            foreach ($entryTypes as $entryType) {
150
                Craft::$app->sections->deleteEntryTypeById($entryType->id);
151
            }
152
        }
153
    }
154
155
    /**
156
     * Save the section manually if it is new to prevent craft from creating the default entry type
157
     * In case of a single we do want the default entry type and do a normal save
158
     * Todo: This method is a bit hackish, find a better way.
159
     *
160
     * @param Section $section
161
     *
162
     * @return mixed
163
     */
164
    private function preSaveSection(Section $section)
165
    {
166
        if ($section->type != 'single' && !$section->id) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $section->id of type integer|null is loosely compared to false; this is ambiguous if the integer can be zero. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
167
            $sectionRecord = new SectionRecord();
168
169
            // Shared attributes
170
            $sectionRecord->name = $section->name;
171
            $sectionRecord->handle = $section->handle;
172
            $sectionRecord->type = $section->type;
173
            $sectionRecord->enableVersioning = $section->enableVersioning;
174
175
            if (!$sectionRecord->save()) {
176
                $section->addErrors(['errors' => $sectionRecord->getErrors()]);
177
178
                return false;
179
            }
180
            $section->id = $sectionRecord->id;
181
182
            return true;
183
        }
184
185
        return Craft::$app->sections->saveSection($section);
186
    }
187
188
    /**
189
     * Populate section.
190
     *
191
     * @param Section $section
192
     * @param array   $sectionDefinition
193
     * @param string  $sectionHandle
194
     */
195
    private function populateSection(Section $section, array $sectionDefinition, $sectionHandle)
196
    {
197
        $section->setAttributes([
198
            'handle' => $sectionHandle,
199
            'name' => $sectionDefinition['name'],
200
            'type' => $sectionDefinition['type'],
201
            'hasUrls' => $sectionDefinition['hasUrls'],
202
            'template' => $sectionDefinition['template'],
203
            'maxLevels' => $sectionDefinition['maxLevels'],
204
            'enableVersioning' => $sectionDefinition['enableVersioning'],
205
        ]);
206
207
        $this->populateSectionLocales($section, $sectionDefinition['locales']);
208
    }
209
210
    /**
211
     * Populate section locales.
212
     *
213
     * @param Section $section
214
     * @param $localeDefinitions
215
     */
216
    private function populateSectionLocales(Section $section, $localeDefinitions)
217
    {
218
        $locales = $section->getLocales();
219
220
        foreach ($localeDefinitions as $localeId => $localeDef) {
221
            $locale = array_key_exists($localeId, $locales) ? $locales[$localeId] : new SectionLocaleModel();
222
223
            $locale->setAttributes([
224
                'locale' => $localeId,
225
                'enabledByDefault' => $localeDef['enabledByDefault'],
226
                'urlFormat' => $localeDef['urlFormat'],
227
                'nestedUrlFormat' => $localeDef['nestedUrlFormat'],
228
            ]);
229
230
            // Todo: Is this a hack? I don't see another way.
231
            // Todo: Might need a sorting order as well? It's NULL at the moment.
232
            Craft::$app->db->createCommand()->insertOrUpdate('locales', [
233
                'locale' => $locale->locale,
234
            ], []);
235
236
            $locales[$localeId] = $locale;
237
        }
238
239
        $section->setLocales($locales);
240
    }
241
242
    /**
243
     * Populate entry type.
244
     *
245
     * @param EntryTypeModel $entryType
246
     * @param array          $entryTypeDefinition
247
     * @param string         $entryTypeHandle
248
     * @param int            $sectionId
249
     */
250
    private function populateEntryType(EntryTypeModel $entryType, array $entryTypeDefinition, $entryTypeHandle, $sectionId)
251
    {
252
        $entryType->setAttributes([
253
            'handle' => $entryTypeHandle,
254
            'sectionId' => $sectionId,
255
            'name' => $entryTypeDefinition['name'],
256
            'hasTitleField' => $entryTypeDefinition['hasTitleField'],
257
            'titleLabel' => $entryTypeDefinition['titleLabel'],
258
            'titleFormat' => $entryTypeDefinition['titleFormat'],
259
        ]);
260
261
        $fieldLayout = Craft::$app->schematic_fields->getFieldLayout($entryTypeDefinition['fieldLayout']);
262
        $entryType->setFieldLayout($fieldLayout);
263
    }
264
}
265