1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace League\Flysystem\Sftp; |
4
|
|
|
|
5
|
|
|
use InvalidArgumentException; |
6
|
|
|
use League\Flysystem\Adapter\AbstractFtpAdapter; |
7
|
|
|
use League\Flysystem\Adapter\Polyfill\StreamedCopyTrait; |
8
|
|
|
use League\Flysystem\AdapterInterface; |
9
|
|
|
use League\Flysystem\Config; |
10
|
|
|
use League\Flysystem\Util; |
11
|
|
|
use LogicException; |
12
|
|
|
use phpseclib\Net\SFTP; |
13
|
|
|
use phpseclib\Crypt\RSA; |
14
|
|
|
use phpseclib\System\SSH\Agent; |
15
|
|
|
use RuntimeException; |
16
|
|
|
|
17
|
|
|
class SftpAdapter extends AbstractFtpAdapter |
18
|
|
|
{ |
19
|
|
|
use StreamedCopyTrait; |
20
|
|
|
|
21
|
|
|
/** |
22
|
|
|
* @var int |
23
|
|
|
*/ |
24
|
|
|
protected $port = 22; |
25
|
|
|
|
26
|
|
|
/** |
27
|
|
|
* @var string |
28
|
|
|
*/ |
29
|
|
|
protected $hostFingerprint; |
30
|
|
|
|
31
|
|
|
/** |
32
|
|
|
* @var string |
33
|
|
|
*/ |
34
|
|
|
protected $privatekey; |
35
|
|
|
|
36
|
|
|
/** |
37
|
|
|
* @var bool |
38
|
|
|
*/ |
39
|
|
|
protected $useAgent = false; |
40
|
|
|
|
41
|
|
|
/** |
42
|
|
|
* @var Agent |
43
|
|
|
*/ |
44
|
|
|
private $agent; |
45
|
|
|
|
46
|
|
|
/** |
47
|
|
|
* @var array |
48
|
|
|
*/ |
49
|
|
|
protected $configurable = ['host', 'hostFingerprint', 'port', 'username', 'password', 'useAgent', 'agent', 'timeout', 'root', 'privateKey', 'permPrivate', 'permPublic', 'directoryPerm', 'NetSftpConnection']; |
50
|
|
|
|
51
|
|
|
/** |
52
|
|
|
* @var array |
53
|
|
|
*/ |
54
|
|
|
protected $statMap = ['mtime' => 'timestamp', 'size' => 'size']; |
55
|
|
|
|
56
|
|
|
/** |
57
|
|
|
* @var int |
58
|
|
|
*/ |
59
|
|
|
protected $directoryPerm = 0744; |
60
|
|
|
|
61
|
|
|
/** |
62
|
|
|
* Prefix a path. |
63
|
|
|
* |
64
|
|
|
* @param string $path |
65
|
|
|
* |
66
|
|
|
* @return string |
67
|
|
|
*/ |
68
|
6 |
|
protected function prefix($path) |
69
|
|
|
{ |
70
|
6 |
|
return $this->root.ltrim($path, $this->separator); |
71
|
|
|
} |
72
|
|
|
|
73
|
|
|
/** |
74
|
|
|
* Set the finger print of the public key of the host you are connecting to. |
75
|
|
|
* |
76
|
|
|
* If the key does not match the server identification, the connection will |
77
|
|
|
* be aborted. |
78
|
|
|
* |
79
|
|
|
* @param string $fingerprint Example: '88:76:75:96:c1:26:7c:dd:9f:87:50:db:ac:c4:a8:7c'. |
80
|
|
|
* |
81
|
|
|
* @return $this |
82
|
|
|
*/ |
83
|
6 |
|
public function setHostFingerprint($fingerprint) |
84
|
|
|
{ |
85
|
6 |
|
$this->hostFingerprint = $fingerprint; |
86
|
|
|
|
87
|
6 |
|
return $this; |
88
|
|
|
} |
89
|
|
|
|
90
|
|
|
/** |
91
|
|
|
* Set the private key (string or path to local file). |
92
|
|
|
* |
93
|
|
|
* @param string $key |
94
|
|
|
* |
95
|
|
|
* @return $this |
96
|
|
|
*/ |
97
|
9 |
|
public function setPrivateKey($key) |
98
|
|
|
{ |
99
|
9 |
|
$this->privatekey = $key; |
100
|
|
|
|
101
|
9 |
|
return $this; |
102
|
|
|
} |
103
|
|
|
|
104
|
|
|
/** |
105
|
|
|
* @param boolean $useAgent |
106
|
|
|
* |
107
|
|
|
* @return $this |
108
|
|
|
*/ |
109
|
|
|
public function setUseAgent($useAgent) |
110
|
|
|
{ |
111
|
|
|
$this->useAgent = (bool) $useAgent; |
112
|
|
|
|
113
|
|
|
return $this; |
114
|
|
|
} |
115
|
|
|
|
116
|
|
|
/** |
117
|
|
|
* @param Agent $agent |
118
|
|
|
* |
119
|
|
|
* @return $this |
120
|
|
|
*/ |
121
|
|
|
public function setAgent(Agent $agent) |
122
|
|
|
{ |
123
|
|
|
$this->agent = $agent; |
124
|
|
|
|
125
|
|
|
return $this; |
126
|
|
|
} |
127
|
|
|
|
128
|
|
|
/** |
129
|
|
|
* Set permissions for new directory |
130
|
|
|
* |
131
|
|
|
* @param int $directoryPerm |
132
|
|
|
* |
133
|
|
|
* @return $this |
134
|
|
|
*/ |
135
|
6 |
|
public function setDirectoryPerm($directoryPerm) |
136
|
|
|
{ |
137
|
6 |
|
$this->directoryPerm = $directoryPerm; |
138
|
|
|
|
139
|
6 |
|
return $this; |
140
|
|
|
} |
141
|
|
|
|
142
|
|
|
/** |
143
|
|
|
* Get permissions for new directory |
144
|
|
|
* |
145
|
|
|
* @return int |
146
|
|
|
*/ |
147
|
3 |
|
public function getDirectoryPerm() |
148
|
|
|
{ |
149
|
3 |
|
return $this->directoryPerm; |
150
|
|
|
} |
151
|
|
|
|
152
|
|
|
/** |
153
|
|
|
* Inject the SFTP instance. |
154
|
|
|
* |
155
|
|
|
* @param SFTP $connection |
156
|
|
|
* |
157
|
|
|
* @return $this |
158
|
|
|
*/ |
159
|
30 |
|
public function setNetSftpConnection(SFTP $connection) |
160
|
2 |
|
{ |
161
|
30 |
|
$this->connection = $connection; |
162
|
|
|
|
163
|
30 |
|
return $this; |
164
|
|
|
} |
165
|
|
|
|
166
|
|
|
/** |
167
|
|
|
* Connect. |
168
|
|
|
*/ |
169
|
21 |
|
public function connect() |
170
|
|
|
{ |
171
|
21 |
|
$this->connection = $this->connection ?: new SFTP($this->host, $this->port, $this->timeout); |
172
|
21 |
|
$this->login(); |
173
|
15 |
|
$this->setConnectionRoot(); |
174
|
12 |
|
} |
175
|
|
|
|
176
|
|
|
/** |
177
|
|
|
* Login. |
178
|
|
|
* |
179
|
|
|
* @throws LogicException |
180
|
|
|
*/ |
181
|
21 |
|
protected function login() |
182
|
|
|
{ |
183
|
21 |
|
if ($this->hostFingerprint) { |
184
|
6 |
|
$actualFingerprint = $this->getHexFingerprintFromSshPublicKey($this->connection->getServerPublicHostKey()); |
185
|
|
|
|
186
|
6 |
|
if (0 !== strcasecmp($this->hostFingerprint, $actualFingerprint)) { |
187
|
3 |
|
throw new LogicException('The authenticity of host '.$this->host.' can\'t be established.'); |
188
|
|
|
} |
189
|
2 |
|
} |
190
|
|
|
|
191
|
18 |
|
$authentication = $this->getAuthentication(); |
192
|
|
|
|
193
|
18 |
|
if (! $this->connection->login($this->username, $authentication)) { |
194
|
3 |
|
throw new LogicException('Could not login with username: '.$this->username.', host: '.$this->host); |
195
|
|
|
} |
196
|
|
|
|
197
|
15 |
|
if ($authentication instanceof Agent) { |
|
|
|
|
198
|
|
|
$authentication->startSSHForwarding($this->connection); |
199
|
|
|
} |
200
|
15 |
|
} |
201
|
|
|
|
202
|
|
|
/** |
203
|
|
|
* Convert the SSH RSA public key into a hex formatted fingerprint. |
204
|
|
|
* |
205
|
|
|
* @param string $publickey |
206
|
|
|
* @return string Hex formatted fingerprint, e.g. '88:76:75:96:c1:26:7c:dd:9f:87:50:db:ac:c4:a8:7c'. |
207
|
|
|
*/ |
208
|
6 |
|
private function getHexFingerprintFromSshPublicKey ($publickey) |
209
|
|
|
{ |
210
|
6 |
|
$content = explode(' ', $publickey, 3); |
211
|
6 |
|
return implode(':', str_split(md5(base64_decode($content[1])), 2)); |
212
|
|
|
} |
213
|
|
|
|
214
|
|
|
/** |
215
|
|
|
* Set the connection root. |
216
|
|
|
*/ |
217
|
15 |
|
protected function setConnectionRoot() |
218
|
|
|
{ |
219
|
15 |
|
$root = $this->getRoot(); |
220
|
|
|
|
221
|
15 |
|
if (! $root) { |
|
|
|
|
222
|
9 |
|
return; |
223
|
|
|
} |
224
|
|
|
|
225
|
6 |
|
if (! $this->connection->chdir($root)) { |
226
|
3 |
|
throw new RuntimeException('Root is invalid or does not exist: '.$root); |
227
|
|
|
} |
228
|
3 |
|
$this->root = $this->connection->pwd() . $this->separator; |
229
|
3 |
|
} |
230
|
|
|
|
231
|
|
|
/** |
232
|
|
|
* Get the password, either the private key or a plain text password. |
233
|
|
|
* |
234
|
|
|
* @return Agent|RSA|string |
235
|
|
|
*/ |
236
|
21 |
|
public function getAuthentication() |
237
|
|
|
{ |
238
|
21 |
|
if ($this->useAgent) { |
239
|
|
|
return $this->getAgent(); |
240
|
|
|
} |
241
|
|
|
|
242
|
21 |
|
if ($this->privatekey) { |
243
|
3 |
|
return $this->getPrivateKey(); |
244
|
|
|
} |
245
|
|
|
|
246
|
18 |
|
return $this->getPassword(); |
247
|
|
|
} |
248
|
|
|
|
249
|
|
|
/** |
250
|
|
|
* Get the password, a plain text password. |
251
|
|
|
* |
252
|
|
|
* @return string |
253
|
|
|
*/ |
254
|
18 |
|
public function getPassword() |
255
|
|
|
{ |
256
|
18 |
|
return $this->password; |
257
|
|
|
} |
258
|
|
|
|
259
|
|
|
/** |
260
|
|
|
* Get the private get with the password or private key contents. |
261
|
|
|
* |
262
|
|
|
* @return RSA |
263
|
|
|
*/ |
264
|
9 |
|
public function getPrivateKey() |
265
|
|
|
{ |
266
|
9 |
|
if (@is_file($this->privatekey)) { |
267
|
3 |
|
$this->privatekey = file_get_contents($this->privatekey); |
268
|
2 |
|
} |
269
|
|
|
|
270
|
9 |
|
$key = new RSA(); |
271
|
|
|
|
272
|
9 |
|
if ($this->password) { |
|
|
|
|
273
|
9 |
|
$key->setPassword($this->password); |
274
|
6 |
|
} |
275
|
|
|
|
276
|
9 |
|
$key->loadKey($this->privatekey); |
277
|
|
|
|
278
|
9 |
|
return $key; |
279
|
|
|
} |
280
|
|
|
|
281
|
|
|
/** |
282
|
|
|
* @return Agent|bool |
283
|
|
|
*/ |
284
|
|
|
public function getAgent() |
285
|
|
|
{ |
286
|
|
|
if ( ! $this->agent instanceof Agent) { |
|
|
|
|
287
|
|
|
$this->agent = new Agent(); |
288
|
|
|
} |
289
|
|
|
|
290
|
|
|
return $this->agent; |
291
|
|
|
} |
292
|
|
|
|
293
|
|
|
/** |
294
|
|
|
* List the contents of a directory. |
295
|
|
|
* |
296
|
|
|
* @param string $directory |
297
|
|
|
* @param bool $recursive |
298
|
|
|
* |
299
|
|
|
* @return array |
300
|
|
|
*/ |
301
|
6 |
|
protected function listDirectoryContents($directory, $recursive = true) |
302
|
|
|
{ |
303
|
6 |
|
$result = []; |
304
|
6 |
|
$connection = $this->getConnection(); |
305
|
6 |
|
$location = $this->prefix($directory); |
306
|
6 |
|
$listing = $connection->rawlist($location); |
307
|
|
|
|
308
|
6 |
|
if ($listing === false) { |
309
|
3 |
|
return []; |
310
|
|
|
} |
311
|
|
|
|
312
|
6 |
|
foreach ($listing as $filename => $object) { |
313
|
6 |
|
if (in_array($filename, ['.', '..'])) { |
314
|
3 |
|
continue; |
315
|
|
|
} |
316
|
|
|
|
317
|
6 |
|
$path = empty($directory) ? $filename : ($directory.'/'.$filename); |
318
|
6 |
|
$result[] = $this->normalizeListingObject($path, $object); |
319
|
|
|
|
320
|
6 |
|
if ($recursive && $object['type'] === NET_SFTP_TYPE_DIRECTORY) { |
321
|
4 |
|
$result = array_merge($result, $this->listDirectoryContents($path)); |
322
|
2 |
|
} |
323
|
4 |
|
} |
324
|
|
|
|
325
|
6 |
|
return $result; |
326
|
|
|
} |
327
|
|
|
|
328
|
|
|
/** |
329
|
|
|
* Normalize a listing response. |
330
|
|
|
* |
331
|
|
|
* @param string $path |
332
|
|
|
* @param array $object |
333
|
|
|
* |
334
|
|
|
* @return array |
335
|
|
|
*/ |
336
|
6 |
|
protected function normalizeListingObject($path, array $object) |
337
|
|
|
{ |
338
|
6 |
|
$permissions = $this->normalizePermissions($object['permissions']); |
339
|
6 |
|
$type = ($object['type'] === 1) ? 'file' : 'dir' ; |
340
|
6 |
|
$timestamp = $object['mtime']; |
341
|
|
|
|
342
|
6 |
|
if ($type === 'dir') { |
343
|
6 |
|
return compact('path', 'timestamp', 'type'); |
344
|
|
|
} |
345
|
|
|
|
346
|
6 |
|
$visibility = $permissions & 0044 ? AdapterInterface::VISIBILITY_PUBLIC : AdapterInterface::VISIBILITY_PRIVATE; |
347
|
6 |
|
$size = (int) $object['size']; |
348
|
|
|
|
349
|
6 |
|
return compact('path', 'timestamp', 'type', 'visibility', 'size'); |
350
|
|
|
} |
351
|
|
|
|
352
|
|
|
/** |
353
|
|
|
* Disconnect. |
354
|
|
|
*/ |
355
|
15 |
|
public function disconnect() |
356
|
|
|
{ |
357
|
15 |
|
$this->connection = null; |
358
|
15 |
|
} |
359
|
|
|
|
360
|
|
|
/** |
361
|
|
|
* @inheritdoc |
362
|
|
|
*/ |
363
|
6 |
|
public function write($path, $contents, Config $config) |
364
|
|
|
{ |
365
|
6 |
|
if ($this->upload($path, $contents, $config) === false) { |
366
|
6 |
|
return false; |
367
|
|
|
} |
368
|
|
|
|
369
|
6 |
|
return compact('contents', 'visibility', 'path'); |
370
|
|
|
} |
371
|
|
|
|
372
|
|
|
/** |
373
|
|
|
* @inheritdoc |
374
|
|
|
*/ |
375
|
6 |
|
public function writeStream($path, $resource, Config $config) |
376
|
|
|
{ |
377
|
6 |
|
if ($this->upload($path, $resource, $config) === false) { |
378
|
6 |
|
return false; |
379
|
|
|
} |
380
|
|
|
|
381
|
6 |
|
return compact('visibility', 'path'); |
382
|
|
|
} |
383
|
|
|
|
384
|
|
|
/** |
385
|
|
|
* Upload a file. |
386
|
|
|
* |
387
|
|
|
* @param string $path |
388
|
|
|
* @param string|resource $contents |
389
|
|
|
* @param Config $config |
390
|
|
|
* @return bool |
391
|
|
|
*/ |
392
|
12 |
|
public function upload($path, $contents, Config $config) |
393
|
|
|
{ |
394
|
12 |
|
$connection = $this->getConnection(); |
395
|
12 |
|
$this->ensureDirectory(Util::dirname($path)); |
396
|
12 |
|
$config = Util::ensureConfig($config); |
397
|
|
|
|
398
|
12 |
|
if (! $connection->put($path, $contents, SFTP::SOURCE_STRING)) { |
399
|
12 |
|
return false; |
400
|
|
|
} |
401
|
|
|
|
402
|
12 |
|
if ($config && $visibility = $config->get('visibility')) { |
403
|
6 |
|
$this->setVisibility($path, $visibility); |
404
|
4 |
|
} |
405
|
|
|
|
406
|
12 |
|
return true; |
407
|
|
|
} |
408
|
|
|
|
409
|
|
|
/** |
410
|
|
|
* @inheritdoc |
411
|
|
|
*/ |
412
|
6 |
|
public function read($path) |
413
|
|
|
{ |
414
|
6 |
|
$connection = $this->getConnection(); |
415
|
|
|
|
416
|
6 |
|
if (($contents = $connection->get($path)) === false) { |
417
|
6 |
|
return false; |
418
|
|
|
} |
419
|
|
|
|
420
|
6 |
|
return compact('contents'); |
421
|
|
|
} |
422
|
|
|
|
423
|
|
|
/** |
424
|
|
|
* @inheritdoc |
425
|
|
|
*/ |
426
|
3 |
|
public function readStream($path) |
427
|
|
|
{ |
428
|
3 |
|
$stream = tmpfile(); |
429
|
3 |
|
$connection = $this->getConnection(); |
430
|
|
|
|
431
|
3 |
|
if ($connection->get($path, $stream) === false) { |
432
|
3 |
|
fclose($stream); |
433
|
3 |
|
return false; |
434
|
|
|
} |
435
|
|
|
|
436
|
3 |
|
rewind($stream); |
437
|
|
|
|
438
|
3 |
|
return compact('stream'); |
439
|
|
|
} |
440
|
|
|
|
441
|
|
|
/** |
442
|
|
|
* @inheritdoc |
443
|
|
|
*/ |
444
|
3 |
|
public function update($path, $contents, Config $config) |
445
|
|
|
{ |
446
|
3 |
|
return $this->write($path, $contents, $config); |
447
|
|
|
} |
448
|
|
|
|
449
|
|
|
/** |
450
|
|
|
* @inheritdoc |
451
|
|
|
*/ |
452
|
3 |
|
public function updateStream($path, $contents, Config $config) |
453
|
|
|
{ |
454
|
3 |
|
return $this->writeStream($path, $contents, $config); |
455
|
|
|
} |
456
|
|
|
|
457
|
|
|
/** |
458
|
|
|
* @inheritdoc |
459
|
|
|
*/ |
460
|
3 |
|
public function delete($path) |
461
|
|
|
{ |
462
|
3 |
|
$connection = $this->getConnection(); |
463
|
|
|
|
464
|
3 |
|
return $connection->delete($path); |
465
|
|
|
} |
466
|
|
|
|
467
|
|
|
/** |
468
|
|
|
* @inheritdoc |
469
|
|
|
*/ |
470
|
3 |
|
public function rename($path, $newpath) |
471
|
|
|
{ |
472
|
3 |
|
$connection = $this->getConnection(); |
473
|
|
|
|
474
|
3 |
|
return $connection->rename($path, $newpath); |
475
|
|
|
} |
476
|
|
|
|
477
|
|
|
/** |
478
|
|
|
* @inheritdoc |
479
|
|
|
*/ |
480
|
3 |
|
public function deleteDir($dirname) |
481
|
|
|
{ |
482
|
3 |
|
$connection = $this->getConnection(); |
483
|
|
|
|
484
|
3 |
|
return $connection->delete($dirname, true); |
485
|
|
|
} |
486
|
|
|
|
487
|
|
|
/** |
488
|
|
|
* @inheritdoc |
489
|
|
|
*/ |
490
|
39 |
|
public function has($path) |
491
|
|
|
{ |
492
|
39 |
|
return $this->getMetadata($path); |
493
|
|
|
} |
494
|
|
|
|
495
|
|
|
/** |
496
|
|
|
* @inheritdoc |
497
|
|
|
*/ |
498
|
48 |
|
public function getMetadata($path) |
499
|
|
|
{ |
500
|
48 |
|
$connection = $this->getConnection(); |
501
|
48 |
|
$info = $connection->stat($path); |
502
|
|
|
|
503
|
48 |
|
if ($info === false) { |
504
|
12 |
|
return false; |
505
|
|
|
} |
506
|
|
|
|
507
|
39 |
|
$result = Util::map($info, $this->statMap); |
508
|
39 |
|
$result['type'] = $info['type'] === NET_SFTP_TYPE_DIRECTORY ? 'dir' : 'file'; |
509
|
39 |
|
$result['visibility'] = $info['permissions'] & $this->permPublic ? 'public' : 'private'; |
510
|
|
|
|
511
|
39 |
|
return $result; |
512
|
|
|
} |
513
|
|
|
|
514
|
|
|
/** |
515
|
|
|
* @inheritdoc |
516
|
|
|
*/ |
517
|
6 |
|
public function getTimestamp($path) |
518
|
|
|
{ |
519
|
6 |
|
return $this->getMetadata($path); |
520
|
2 |
|
} |
521
|
|
|
|
522
|
|
|
/** |
523
|
|
|
* @inheritdoc |
524
|
|
|
*/ |
525
|
3 |
|
public function getMimetype($path) |
526
|
|
|
{ |
527
|
3 |
|
if (! $data = $this->read($path)) { |
528
|
3 |
|
return false; |
529
|
|
|
} |
530
|
|
|
|
531
|
3 |
|
$data['mimetype'] = Util::guessMimeType($path, $data['contents']); |
532
|
|
|
|
533
|
3 |
|
return $data; |
534
|
|
|
} |
535
|
|
|
|
536
|
|
|
/** |
537
|
|
|
* @inheritdoc |
538
|
|
|
*/ |
539
|
3 |
|
public function createDir($dirname, Config $config) |
540
|
|
|
{ |
541
|
3 |
|
$connection = $this->getConnection(); |
542
|
|
|
|
543
|
3 |
|
if (! $connection->mkdir($dirname, $this->directoryPerm, true)) { |
544
|
3 |
|
return false; |
545
|
|
|
} |
546
|
|
|
|
547
|
3 |
|
return ['path' => $dirname]; |
548
|
|
|
} |
549
|
|
|
|
550
|
|
|
/** |
551
|
|
|
* @inheritdoc |
552
|
|
|
*/ |
553
|
6 |
|
public function getVisibility($path) |
554
|
|
|
{ |
555
|
6 |
|
return $this->getMetadata($path); |
556
|
|
|
} |
557
|
|
|
|
558
|
|
|
/** |
559
|
|
|
* @inheritdoc |
560
|
|
|
*/ |
561
|
12 |
|
public function setVisibility($path, $visibility) |
562
|
|
|
{ |
563
|
12 |
|
$visibility = ucfirst($visibility); |
564
|
|
|
|
565
|
12 |
|
if (! isset($this->{'perm'.$visibility})) { |
566
|
3 |
|
throw new InvalidArgumentException('Unknown visibility: '.$visibility); |
567
|
|
|
} |
568
|
|
|
|
569
|
9 |
|
$connection = $this->getConnection(); |
570
|
|
|
|
571
|
9 |
|
return $connection->chmod($this->{'perm'.$visibility}, $path); |
572
|
|
|
} |
573
|
|
|
|
574
|
|
|
/** |
575
|
|
|
* @inheritdoc |
576
|
|
|
*/ |
577
|
6 |
|
public function isConnected() |
578
|
|
|
{ |
579
|
6 |
|
if ($this->connection instanceof SFTP && $this->connection->isConnected()) { |
|
|
|
|
580
|
3 |
|
return true; |
581
|
|
|
} |
582
|
|
|
|
583
|
3 |
|
return false; |
584
|
|
|
} |
585
|
|
|
} |
586
|
|
|
|
This error could be the result of:
1. Missing dependencies
PHP Analyzer uses your
composer.json
file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects thecomposer.json
to be in the root folder of your repository.Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the
require
orrequire-dev
section?2. Missing use statement
PHP does not complain about undefined classes in
ìnstanceof
checks. For example, the following PHP code will work perfectly fine:If you have not tested against this specific condition, such errors might go unnoticed.