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