• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

heimrichhannot / contao-utils-bundle / 13965748749

20 Mar 2025 09:13AM UTC coverage: 79.29% (-0.03%) from 79.32%
13965748749

push

github

koertho
adjust rector config

1095 of 1381 relevant lines covered (79.29%)

3.28 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

91.67
/src/Util/ModelUtil.php
1
<?php
2

3
/*
4
 * Copyright (c) 2023 Heimrich & Hannot GmbH
5
 *
6
 * @license LGPL-3.0-or-later
7
 */
8

9
namespace HeimrichHannot\UtilsBundle\Util;
10

11
use Contao\CoreBundle\Framework\Adapter;
12
use Contao\CoreBundle\Framework\ContaoFramework;
13
use Contao\CoreBundle\InsertTag\InsertTagParser;
14
use Contao\CoreBundle\Security\Authentication\Token\TokenChecker;
15
use Contao\Date;
16
use Contao\Model;
17
use Contao\Model\Collection;
18

19
class ModelUtil
20
{
21
    public function __construct(
22
        private readonly ContaoFramework $framework,
23
        private readonly InsertTagParser $insertTagParser,
24
        private readonly TokenChecker $tokenChecker,
25
    )
26
    {
27
    }
7✔
28

29
    /**
30
     * Adds an published check to your model query.
31
     *
32
     * Options:
33
     * - publishedField: The name of the published field. Default: "published"
34
     * - startField: The name of the start field. Default: "start"
35
     * - stopField: The name of the stop field. Default: "stop"
36
     * - invertPublishedField: Set to true, if the published field should be evaluated inverted (for "hidden" or "invisible" fields. Default: false
37
     * - invertStartStopFields: Set to true, if the start and stop fields should be evaluated in an inverted manner. Default: false
38
     * - ignoreFePreview: Set to true, frontend preview should be ignored. Default: false
39
     *
40
     * @param string $table   The table name
41
     * @param array  $columns The columns array
42
     * @param array{
43
     *     publishedField?: string,
44
     *     startField?: string,
45
     *     stopField?: string,
46
     *     invertPublishedField?: bool,
47
     *     invertStartStopFields?: bool,
48
     *     ignoreFePreview?: bool
49
     * }  $options pass additional options
50
     */
51
    public function addPublishedCheckToModelArrays(string $table, array &$columns, array $options = []): void
52
    {
53
        $defaults = [
1✔
54
            'invertPublishedField' => false,
1✔
55
            'invertStartStopFields' => false,
1✔
56
            'publishedField' => 'published',
1✔
57
            'startField' => 'start',
1✔
58
            'stopField' => 'stop',
1✔
59
            'ignoreFePreview' => false,
1✔
60
        ];
1✔
61
        $options = array_merge($defaults, $options);
1✔
62

63
        $t = $table;
1✔
64

65
        if ($options['ignoreFePreview'] || !$this->tokenChecker->isPreviewMode()) {
1✔
66
            $time = Date::floorToMinute();
1✔
67

68
            $columns[] = "($t.".$options['startField'].($options['invertStartStopFields'] ? '!=' : '=')."'' OR $t.".$options['startField'].($options['invertStartStopFields'] ? '>' : '<=')."'$time') AND ($t.".$options['stopField'].($options['invertStartStopFields'] ? '!=' : '=')."'' OR $t.".$options['stopField'].($options['invertStartStopFields'] ? '<=' : '>')."'".($time + 60)."') AND $t.".$options['publishedField'].($options['invertPublishedField'] ? '!=' : '=')."'1'";
1✔
69
        }
70
    }
71

72
    /**
73
     * Returns model instances by given table and search criteria.
74
     *
75
     * Options:
76
     * - skipReplaceInsertTags: (bool) Skip the replacement of inserttags. Default: false
77
     *
78
     * @param array{
79
     *     skipReplaceInsertTags?: bool
80
     * } $options
81
     *
82
     * @return Model[]|Collection|null
83
     */
84
    public function findModelInstancesBy(string $table, array|string|null $columns, int|string|array|null $values, array $options = []): Collection|Model|null
85
    {
86
        $defaults = [
1✔
87
            'skipReplaceInsertTags' => false,
1✔
88
        ];
1✔
89
        $options = array_merge($defaults, $options);
1✔
90

91
        /* @var string|null $modelClass */
92
        if (!($modelClass = $this->framework->getAdapter(Model::class)->getClassFromTable($table))) {
1✔
93
            return null;
1✔
94
        }
95

96
        /* @var Model $adapter */
97
        if (null === ($adapter = $this->framework->getAdapter($modelClass))) {
1✔
98
            return null;
×
99
        }
100

101
        if (is_array($values) && !$options['skipReplaceInsertTags']) {
1✔
102
            $values = array_map(fn($value) => $this->insertTagParser->replace($value), $values);
1✔
103
        }
104

105
        if (empty($columns)) {
1✔
106
            $columns = null;
1✔
107
        }
108

109
        return $adapter->findBy($columns, $values, $options);
1✔
110
    }
111

112
    /**
113
     * Find a single model instance for given table by its primary key (id).
114
     *
115
     * @param string $table The table
116
     * @param int|string $pk The property value
117
     * @param array $options An optional options array
118
     *
119
     * @return Model|null The model or null if the result is empty
120
     */
121
    public function findModelInstanceByPk(string $table, int|string $pk, array $options = []): ?Model
122
    {
123
        if (!($modelClass = $this->framework->getAdapter(Model::class)->getClassFromTable($table))) {
1✔
124
            return null;
1✔
125
        }
126

127
        if (null === ($adapter = $this->framework->getAdapter($modelClass))) {
1✔
128
            return null;
×
129
        }
130

131
        /** @var Model|Adapter $adapter */
132
        return $adapter->findByPk($pk, $options);
1✔
133
    }
134

135
    /**
136
     * Return a single model instance by table and search criteria.
137
     *
138
     * Options:
139
     * - skipReplaceInsertTags: Skip the replacement of inserttags. Default: false
140
     *
141
     * @param array{
142
     *     skipReplaceInsertTags?: bool
143
     * } $options
144
     *
145
     */
146
    public function findOneModelInstanceBy(string $table, array $columns, array $values, array $options = []): ?Model
147
    {
148
        $options = array_merge([
1✔
149
            'skipReplaceInsertTags' => false,
1✔
150
        ], $options);
1✔
151

152
        if (!($modelClass = $this->framework->getAdapter(Model::class)->getClassFromTable($table))) {
1✔
153
            return null;
1✔
154
        }
155

156

157
        if (null === ($adapter = $this->framework->getAdapter($modelClass))) {
1✔
158
            return null;
×
159
        }
160

161
        if (is_array($values) && !$options['skipReplaceInsertTags']) {
1✔
162
            $values = array_map(fn($value) => $this->insertTagParser->replace($value), $values);
1✔
163
        }
164

165
        if (empty($columns)) {
1✔
166
            $columns = null;
1✔
167
        }
168

169
        /* @var Model|Adapter $adapter */
170
        return $adapter->findOneBy($columns, $values, $options);
1✔
171
    }
172

173
    /**
174
     * Returns multiple model instances by given table and ids.
175
     */
176
    public function findMultipleModelInstancesByIds(string $table, array $ids, array $options = []): Collection|Model|null
177
    {
178
        if (!($modelClass = $this->framework->getAdapter(Model::class)->getClassFromTable($table))) {
1✔
179
            return null;
1✔
180
        }
181

182
        if (null === ($adapter = $this->framework->getAdapter($modelClass))) {
1✔
183
            return null;
×
184
        }
185

186
        /** @var Model|Adapter $adapter */
187
        return $adapter->findBy(["$table.id IN(".implode(',', array_map('\intval', $ids)).')'], null, $options);
1✔
188
    }
189

190
    /**
191
     * Returns model instance by given table and id or alias.
192
     */
193
    public function findModelInstanceByIdOrAlias(string $table, int|string $idOrAlias, array $options = []): ?Model
194
    {
195
        if (!($modelClass = $this->framework->getAdapter(Model::class)->getClassFromTable($table))) {
1✔
196
            return null;
1✔
197
        }
198

199
        /* @var Model $adapter */
200
        if (null === ($adapter = $this->framework->getAdapter($modelClass))) {
1✔
201
            return null;
×
202
        }
203

204
        $options = array_merge(
1✔
205
            [
1✔
206
                'limit' => 1,
1✔
207
                'column' => !is_numeric($idOrAlias) ? ["$table.alias=?"] : ["$table.id=?"],
1✔
208
                'value' => $idOrAlias,
1✔
209
                'return' => 'Model',
1✔
210
            ],
1✔
211
            $options
1✔
212
        );
1✔
213

214
        return $adapter->findByIdOrAlias($idOrAlias, $options);
1✔
215
    }
216

217
    /**
218
     * Returns an array of a model instance's parents in ascending order, i.e. the root parent comes first.
219
     *
220
     * @template T of Model
221
     * @param T $instance
222
     * @param string $parentProperty
223
     * @return array<T>
224
     */
225
    public function findParentsRecursively(Model $instance, string $parentProperty = 'pid'): array
226
    {
227
        $table = call_user_func($instance->getTable(...));
1✔
228

229
        $parents = [];
1✔
230
        $model = $this->framework->getAdapter(Model::class);
1✔
231
        $modelClass = $model->getClassFromTable($table);
1✔
232

233
        if (!$instance->{$parentProperty}) {
1✔
234
            return $parents;
1✔
235
        }
236

237
        if (null === ($parentInstance = $this->framework->getAdapter($modelClass)->findByPk($instance->{$parentProperty}))) {
1✔
238
            return $parents;
×
239
        }
240

241
        return array_merge($this->findParentsRecursively($parentInstance, $parentProperty), [$parentInstance]);
1✔
242
    }
243
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc