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

visavi / motor-orm / 30439216777

29 Jul 2026 09:19AM UTC coverage: 98.938% (-0.5%) from 99.407%
30439216777

push

github

visavi
Добавлено замыкание в with() и чтение страницы пагинации из запроса

    В with() можно передать замыкание, сужающее связь на одну загрузку: имя
    ключом, замыкание значением, обычные имена лежат в том же списке.

        Story::query()->with([
            user,
            comments => static fn (Query $query) => $query->where(approved, 1),
        ])->get();

    constrain() остаётся: он описывает связь, которая всегда такая, и работает
    при ленивом чтении. Когда есть оба, условия складываются.

    Из paginate() и simplePaginate() убран номер страницы, вместо него добавлен
    Query::page().

        Article::query()->paginate(10);           // ?page= из запроса
        Article::query()->page(3)->paginate(10);  // сказано прямо

    В PagedCollection добавлены статические resolvePageUsing(),
    resolveCurrentPage() и pageName(), а setPageName() стал статическим:
    страницу надо знать до того, как появится сама страница строк. По
    умолчанию страница читается из $_GET, resolvePageUsing() подменяет
    источник для сред, где $_GET не заполняется.

    Ломающее: второй аргумент paginate() и simplePaginate() убран совсем,
    setPageName() больше не вызывается на объекте.

39 of 44 new or added lines in 3 files covered. (88.64%)

1025 of 1036 relevant lines covered (98.94%)

27.84 hits per line

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

96.72
/src/PagedCollection.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace MotorORM;
6

7
use Closure;
8
use InvalidArgumentException;
9

10
/**
11
 * A collection that knows it is one page of a longer read
12
 *
13
 * It holds the rows of that page and is walked over like any other collection,
14
 * and on top of that it knows where the page stands among the rest. Where the
15
 * page number came from is none of its business
16
 *
17
 * @license Code and contributions have MIT License
18
 * @link    https://visavi.net
19
 * @author  Alexander Grigorev <admin@visavi.net>
20
 */
21
abstract class PagedCollection extends Collection
22
{
23
    /** Rows on a page */
24
    protected(set) int $limit;
25

26
    /** The page being shown */
27
    protected(set) int $page;
28

29
    /** Rows to skip to reach the page */
30
    protected(set) int $offset;
31

32
    /**
33
     * Name of the page parameter, in the urls that are built and in the
34
     * request the current page is read from
35
     *
36
     * The page has to be known before a page of rows exists, so the name of
37
     * the parameter carrying it cannot belong to that page either
38
     */
39
    private static string $pageName = 'page';
40

41
    /** Where the current page comes from when a query is not told it */
42
    private static ?Closure $pageResolver = null;
43

44
    /** Path the links point at, relative to the current one when it is null */
45
    protected(set) ?string $path = null;
46

47
    /** Query parameters every link carries along */
48
    protected(set) array $appends = [];
49

50
    /** Template the links are rendered with */
51
    protected string $viewPath = __DIR__ . '/views/bootstrap5.php';
52

53
    /**
54
     * @param array $items the rows of the page
55
     * @param int   $limit rows on a page
56
     * @param int   $page  the page to show
57
     */
58
    protected function __construct(array $items, int $limit, int $page)
61✔
59
    {
60
        if ($limit < 1) {
61✔
61
            throw new InvalidArgumentException(
2✔
62
                sprintf('%s() a page has to hold at least one row, %d given', static::class, $limit)
2✔
63
            );
2✔
64
        }
65

66
        $this->limit  = $limit;
59✔
67
        $this->page   = $this->clamp(max(1, $page));
59✔
68
        $this->offset = $this->page * $this->limit - $this->limit;
59✔
69

70
        parent::__construct($items);
59✔
71
    }
72

73
    /**
74
     * The entries of the navigation
75
     *
76
     * The rows of the page are the collection itself, these are the links
77
     * under it
78
     *
79
     * @return Page[]
80
     */
81
    abstract public function pages(): array;
82

83
    /**
84
     * Whether there is anything after this page
85
     *
86
     * @return bool
87
     */
88
    abstract public function hasMorePages(): bool;
89

90
    /**
91
     * Rows on the page being shown
92
     *
93
     * @return int
94
     */
95
    abstract protected function rowsOnPage(): int;
96

97
    /**
98
     * Get current page
99
     *
100
     * @return int
101
     */
102
    public function currentPage(): int
10✔
103
    {
104
        return $this->page;
10✔
105
    }
106

107
    /**
108
     * Rows on a page
109
     *
110
     * @return int
111
     */
112
    public function perPage(): int
4✔
113
    {
114
        return $this->limit;
4✔
115
    }
116

117
    /**
118
     * Number of the first row of the page among all the rows
119
     *
120
     * @return int|null null when the page holds nothing
121
     */
122
    public function firstItem(): ?int
11✔
123
    {
124
        return $this->rowsOnPage() ? $this->offset + 1 : null;
11✔
125
    }
126

127
    /**
128
     * Number of the last row of the page among all the rows
129
     *
130
     * @return int|null null when the page holds nothing
131
     */
132
    public function lastItem(): ?int
10✔
133
    {
134
        return $this->rowsOnPage() ? $this->offset + $this->rowsOnPage() : null;
10✔
135
    }
136

137
    /**
138
     * Whether the first page is the one being shown
139
     *
140
     * @return bool
141
     */
142
    public function onFirstPage(): bool
13✔
143
    {
144
        return $this->page === 1;
13✔
145
    }
146

147
    /**
148
     * Whether the last page is the one being shown
149
     *
150
     * @return bool
151
     */
152
    public function onLastPage(): bool
7✔
153
    {
154
        return ! $this->hasMorePages();
7✔
155
    }
156

157
    /**
158
     * Whether there is more than one page
159
     *
160
     * @return bool
161
     */
162
    public function hasPages(): bool
3✔
163
    {
164
        return $this->hasMorePages() || ! $this->onFirstPage();
3✔
165
    }
166

167
    /**
168
     * Url of a page
169
     *
170
     * @param int $page
171
     *
172
     * @return string
173
     */
174
    public function url(int $page): string
1✔
175
    {
176
        return $this->buildUrl(max(1, $page));
1✔
177
    }
178

179
    /**
180
     * Get rendered links
181
     *
182
     * @param string|null $view a template for this call, the built-in one by default
183
     *
184
     * @return string
185
     */
186
    public function links(?string $view = null): string
12✔
187
    {
188
        /*
189
         * A template renders, so it is handed the pages and nothing else. The
190
         * arguments are the scope the template is given: $pages is what it
191
         * iterates, and being static this closure keeps $this away from it
192
         */
193
        $render = static function (string $view, array $pages): void {
12✔
194
            include $view;
12✔
195
        };
12✔
196

197
        ob_start();
12✔
198

199
        try {
200
            $render($view ?? $this->viewPath, $this->pages());
12✔
201
        } finally {
202
            /* A template that blew up leaves no buffer behind */
203
            $html = ob_get_clean();
12✔
204
        }
205

206
        return $html;
11✔
207
    }
208

209
    /**
210
     * Name the page parameter
211
     *
212
     * @param string $name
213
     *
214
     * @return void
215
     */
216
    public static function setPageName(string $name): void
60✔
217
    {
218
        self::$pageName = $name;
60✔
219
    }
220

221
    /**
222
     * Name of the page parameter
223
     *
224
     * @return string
225
     */
NEW
226
    public static function pageName(): string
×
227
    {
NEW
228
        return self::$pageName;
×
229
    }
230

231
    /**
232
     * Say where the current page comes from
233
     *
234
     * The library reads the query string of the request and nothing else. Any
235
     * other source — a PSR-7 request, a router, a console argument — is named
236
     * here, and null puts the query string back
237
     *
238
     * @param Closure|null $resolver called with the name of the page parameter
239
     *
240
     * @return void
241
     */
242
    public static function resolvePageUsing(?Closure $resolver): void
60✔
243
    {
244
        self::$pageResolver = $resolver;
60✔
245
    }
246

247
    /**
248
     * The page being asked for
249
     *
250
     * @return int never below the first page
251
     */
252
    public static function resolveCurrentPage(): int
13✔
253
    {
254
        if (self::$pageResolver) {
13✔
255
            return max(1, (int) (self::$pageResolver)(self::$pageName));
2✔
256
        }
257

258
        $page = $_GET[self::$pageName] ?? null;
11✔
259

260
        /* A page that is no number is no page, the first one is meant */
261
        return is_numeric($page) ? max(1, (int) $page) : 1;
11✔
262
    }
263

264
    /**
265
     * Point the links at a path
266
     *
267
     * @param string $path
268
     *
269
     * @return $this
270
     */
271
    public function withPath(string $path): static
15✔
272
    {
273
        $this->path = $path;
15✔
274

275
        return $this;
15✔
276
    }
277

278
    /**
279
     * Carry query parameters along every link
280
     *
281
     * @param array $appends
282
     *
283
     * @return $this
284
     */
285
    public function appends(array $appends): static
6✔
286
    {
287
        $this->appends = $appends;
6✔
288

289
        return $this;
6✔
290
    }
291

292
    /**
293
     * Keep the page within the range the collection knows of
294
     *
295
     * @param int $page
296
     *
297
     * @return int
298
     */
299
    protected function clamp(int $page): int
18✔
300
    {
301
        return $page;
18✔
302
    }
303

304
    /**
305
     * Build a link to the given page
306
     *
307
     * @param int             $page
308
     * @param int|string|null $name what the view prints, the number by default
309
     *
310
     * @return Page
311
     */
312
    protected function link(int $page, int|string|null $name = null): Page
18✔
313
    {
314
        return Page::link($page, $this->buildUrl($page), $name ?? $page);
18✔
315
    }
316

317
    /**
318
     * Build url
319
     *
320
     * The first page is the path itself: "?page=1" points at what the path
321
     * already points at, and two urls for one page is one too many
322
     *
323
     * @param int $page
324
     *
325
     * @return string
326
     */
327
    protected function buildUrl(int $page): string
25✔
328
    {
329
        $query = http_build_query(
25✔
330
            $page > 1 ? [self::$pageName => $page] + $this->appends : $this->appends
25✔
331
        );
25✔
332

333
        if ($query === '') {
25✔
334
            /* An empty url would mean the current one, page parameter and all */
335
            return $this->path ?: '?';
6✔
336
        }
337

338
        return $this->path . '?' . $query;
23✔
339
    }
340
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc