1<?php
2/**
3 * Elasticsearch PHP client
4 *
5 * @link      https://github.com/elastic/elasticsearch-php/
6 * @copyright Copyright (c) Elasticsearch B.V (https://www.elastic.co)
7 * @license   http://www.apache.org/licenses/LICENSE-2.0 Apache License, Version 2.0
8 * @license   https://www.gnu.org/licenses/lgpl-2.1.html GNU Lesser General Public License, Version 2.1
9 *
10 * Licensed to Elasticsearch B.V under one or more agreements.
11 * Elasticsearch B.V licenses this file to you under the Apache 2.0 License or
12 * the GNU Lesser General Public License, Version 2.1, at your option.
13 * See the LICENSE file in the project root for more information.
14 */
15
16
17declare(strict_types = 1);
18
19namespace Elasticsearch\Helper\Iterators;
20
21use Iterator;
22
23class SearchHitIterator implements Iterator, \Countable
24{
25
26    /**
27     * @var SearchResponseIterator
28     */
29    private $search_responses;
30
31    /**
32     * @var int
33     */
34    protected $current_key;
35
36    /**
37     * @var int
38     */
39    protected $current_hit_index;
40
41    /**
42     * @var array|null
43     */
44    protected $current_hit_data;
45
46    /**
47     * @var int
48     */
49    protected $count = 0;
50
51    /**
52     * Constructor
53     *
54     * @param SearchResponseIterator $search_responses
55     */
56    public function __construct(SearchResponseIterator $search_responses)
57    {
58        $this->search_responses = $search_responses;
59    }
60
61    /**
62     * Rewinds the internal SearchResponseIterator and itself
63     *
64     * @return void
65     * @see    Iterator::rewind()
66     */
67    public function rewind(): void
68    {
69        $this->current_key = 0;
70        $this->search_responses->rewind();
71
72        // The first page may be empty. In that case, the next page is fetched.
73        $current_page = $this->search_responses->current();
74        if ($this->search_responses->valid() && empty($current_page['hits']['hits'])) {
75            $this->search_responses->next();
76        }
77
78        $this->count = 0;
79        if (isset($current_page['hits']) && isset($current_page['hits']['total'])) {
80            $this->count = $current_page['hits']['total']['value'] ?? $current_page['hits']['total'];
81        }
82
83        $this->readPageData();
84    }
85
86    /**
87     * Advances pointer of the current hit to the next one in the current page. If there
88     * isn't a next hit in the current page, then it advances the current page and moves the
89     * pointer to the first hit in the page.
90     *
91     * @return void
92     * @see    Iterator::next()
93     */
94    public function next(): void
95    {
96        $this->current_key++;
97        $this->current_hit_index++;
98        $current_page = $this->search_responses->current();
99        if (isset($current_page['hits']['hits'][$this->current_hit_index])) {
100            $this->current_hit_data = $current_page['hits']['hits'][$this->current_hit_index];
101        } else {
102            $this->search_responses->next();
103            $this->readPageData();
104        }
105    }
106
107    /**
108     * Returns a boolean indicating whether or not the current pointer has valid data
109     *
110     * @return bool
111     * @see    Iterator::valid()
112     */
113    public function valid(): bool
114    {
115        return is_array($this->current_hit_data);
116    }
117
118    /**
119     * Returns the current hit
120     *
121     * @return array
122     * @see    Iterator::current()
123     */
124    public function current(): array
125    {
126        return $this->current_hit_data;
127    }
128
129    /**
130     * Returns the current hit index. The hit index spans all pages.
131     *
132     * @return int
133     * @see    Iterator::key()
134     */
135    public function key(): int
136    {
137        return $this->current_key;
138    }
139
140    /**
141     * Advances the internal SearchResponseIterator and resets the current_hit_index to 0
142     *
143     * @internal
144     */
145    private function readPageData(): void
146    {
147        if ($this->search_responses->valid()) {
148            $current_page = $this->search_responses->current();
149            $this->current_hit_index = 0;
150            $this->current_hit_data = $current_page['hits']['hits'][$this->current_hit_index];
151        } else {
152            $this->current_hit_data = null;
153        }
154    }
155
156    /**
157     * {@inheritDoc}
158     */
159    public function count(): int
160    {
161        return $this->count;
162    }
163}
164