1<?php
2
3namespace Elastica;
4
5use Elastica\Bulk\Action;
6use Elastica\Exception\InvalidException;
7
8/**
9 * Single document stored in elastic search.
10 *
11 * @author   Nicolas Ruflin <spam@ruflin.com>
12 */
13class Document extends AbstractUpdateAction
14{
15    public const OP_TYPE_CREATE = Action::OP_TYPE_CREATE;
16
17    /**
18     * Document data.
19     *
20     * @var array Document data
21     */
22    protected $_data = [];
23
24    /**
25     * Whether to use this document to upsert if the document does not exist.
26     *
27     * @var bool
28     */
29    protected $_docAsUpsert = false;
30
31    /**
32     * @var bool
33     */
34    protected $_autoPopulate = false;
35
36    /**
37     * Creates a new document.
38     *
39     * @param string|null  $id    The document ID, if null it will be created
40     * @param array|string $data  Data array
41     * @param Index|string $index Index name
42     */
43    public function __construct(?string $id = null, $data = [], $index = '')
44    {
45        $this->setId($id);
46        $this->setData($data);
47        $this->setIndex($index);
48    }
49
50    /**
51     * @return mixed
52     */
53    public function __get(string $key)
54    {
55        return $this->get($key);
56    }
57
58    /**
59     * @param mixed $value
60     */
61    public function __set(string $key, $value): void
62    {
63        $this->set($key, $value);
64    }
65
66    public function __isset(string $key): bool
67    {
68        return $this->has($key) && null !== $this->get($key);
69    }
70
71    public function __unset(string $key): void
72    {
73        $this->remove($key);
74    }
75
76    /**
77     * Get the value of the given field.
78     *
79     * @param mixed $key
80     *
81     * @throws InvalidException If the given field does not exist
82     *
83     * @return mixed
84     */
85    public function get($key)
86    {
87        if (!$this->has($key)) {
88            throw new InvalidException("Field {$key} does not exist");
89        }
90
91        return $this->_data[$key];
92    }
93
94    /**
95     * Set the value of the given field.
96     *
97     * @param mixed $value
98     *
99     * @throws InvalidException if the current document is a serialized data
100     */
101    public function set(string $key, $value): self
102    {
103        if (!\is_array($this->_data)) {
104            throw new InvalidException('Document data is serialized data. Data creation is forbidden.');
105        }
106        $this->_data[$key] = $value;
107
108        return $this;
109    }
110
111    /**
112     * Returns if the current document has the given field.
113     */
114    public function has(string $key): bool
115    {
116        return \is_array($this->_data) && \array_key_exists($key, $this->_data);
117    }
118
119    /**
120     * Removes a field from the document, by the given key.
121     *
122     * @throws InvalidException if the given field does not exist
123     */
124    public function remove(string $key): self
125    {
126        if (!$this->has($key)) {
127            throw new InvalidException("Field {$key} does not exist");
128        }
129        unset($this->_data[$key]);
130
131        return $this;
132    }
133
134    /**
135     * Adds a file to the index.
136     *
137     * To use this feature you have to call the following command in the
138     * elasticsearch directory:
139     * <code>
140     * ./bin/plugin -install elasticsearch/elasticsearch-mapper-attachments/1.6.0
141     * </code>
142     * This installs the tika file analysis plugin. More infos about supported formats
143     * can be found here: {@link http://tika.apache.org/0.7/formats.html}
144     *
145     * @param string $key      Key to add the file to
146     * @param string $filepath Path to add the file
147     * @param string $mimeType Header mime type
148     */
149    public function addFile(string $key, string $filepath, string $mimeType = ''): self
150    {
151        $value = \base64_encode(\file_get_contents($filepath));
152
153        if ('' !== $mimeType) {
154            $value = ['_content_type' => $mimeType, '_name' => $filepath, '_content' => $value];
155        }
156
157        $this->set($key, $value);
158
159        return $this;
160    }
161
162    /**
163     * Add file content.
164     */
165    public function addFileContent(string $key, string $content): self
166    {
167        return $this->set($key, \base64_encode($content));
168    }
169
170    /**
171     * Adds a geopoint field to the document.
172     *
173     * @param string $key Field key
174     *
175     * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/geo-point.html
176     */
177    public function addGeoPoint(string $key, float $latitude, float $longitude): self
178    {
179        $value = ['lat' => $latitude, 'lon' => $longitude];
180
181        $this->set($key, $value);
182
183        return $this;
184    }
185
186    /**
187     * Overwrites the current document data with the given data.
188     *
189     * @param array|string $data Data array
190     */
191    public function setData($data): self
192    {
193        $this->_data = $data;
194
195        return $this;
196    }
197
198    /**
199     * Returns the document data.
200     *
201     * @return array|string Document data
202     */
203    public function getData()
204    {
205        return $this->_data;
206    }
207
208    public function setDocAsUpsert(bool $value): self
209    {
210        $this->_docAsUpsert = $value;
211
212        return $this;
213    }
214
215    public function getDocAsUpsert(): bool
216    {
217        return $this->_docAsUpsert;
218    }
219
220    public function setAutoPopulate(bool $autoPopulate = true): self
221    {
222        $this->_autoPopulate = $autoPopulate;
223
224        return $this;
225    }
226
227    public function isAutoPopulate(): bool
228    {
229        return $this->_autoPopulate;
230    }
231
232    public function setPipeline(string $pipeline): self
233    {
234        return $this->setParam('pipeline', $pipeline);
235    }
236
237    public function getPipeline(): string
238    {
239        return $this->getParam('pipeline');
240    }
241
242    public function hasPipeline(): bool
243    {
244        return $this->hasParam('pipeline');
245    }
246
247    /**
248     * Returns the document as an array.
249     */
250    public function toArray(): array
251    {
252        $doc = $this->getParams();
253        $doc['_source'] = $this->getData();
254
255        return $doc;
256    }
257
258    /**
259     * @param array|Document $data
260     *
261     * @throws InvalidException If invalid data has been provided
262     */
263    public static function create($data): self
264    {
265        if ($data instanceof self) {
266            return $data;
267        }
268
269        if (\is_array($data)) {
270            return new static('', $data);
271        }
272
273        throw new InvalidException('Failed to create document. Invalid data passed.');
274    }
275}
276