Stand: SMTP-Test, Admin-Mail-Tab, Notifiable-Fix, Lazy-Quill

- Fix: Notifiable-Trait zum User-Model hinzugefuegt (behebt notify()-500er)
- Installer: SMTP-Verbindungstest mit EsmtpTransport + Ueberspringen-Link
- Admin: Neuer E-Mail-Tab mit SMTP-Konfiguration + Verbindungstest
- Admin: Lazy Quill-Initialisierung (nur sichtbare Locale wird geladen)
- Uebersetzungen: 17 neue Mail-Keys in allen 6 Sprachen

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Rhino
2026-03-02 07:30:37 +01:00
commit 2e24a40d68
9633 changed files with 1300799 additions and 0 deletions

View File

@@ -0,0 +1,451 @@
<?php
namespace Illuminate\Http\Client;
use Carbon\CarbonImmutable;
use Closure;
use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\Promise\EachPromise;
use GuzzleHttp\Utils;
use Illuminate\Http\Client\Promises\LazyPromise;
use Illuminate\Support\Defer\DeferredCallback;
use function Illuminate\Support\defer;
/**
* @mixin \Illuminate\Http\Client\Factory
*/
class Batch
{
/**
* The factory instance.
*
* @var \Illuminate\Http\Client\Factory
*/
protected $factory;
/**
* The array of requests.
*
* @var array<array-key, \Illuminate\Http\Client\PendingRequest>
*/
protected $requests = [];
/**
* The total number of requests that belong to the batch.
*
* @var non-negative-int
*/
public $totalRequests = 0;
/**
* The total number of requests that are still pending.
*
* @var non-negative-int
*/
public $pendingRequests = 0;
/**
* The total number of requests that have failed.
*
* @var non-negative-int
*/
public $failedRequests = 0;
/**
* The handler function for the Guzzle client.
*
* @var callable
*/
protected $handler;
/**
* The callback to run before the first request from the batch runs.
*
* @var (\Closure($this): void)|null
*/
protected $beforeCallback = null;
/**
* The callback to run after a request from the batch succeeds.
*
* @var (\Closure($this, int|string, \Illuminate\Http\Client\Response): void)|null
*/
protected $progressCallback = null;
/**
* The callback to run after a request from the batch fails.
*
* @var (\Closure($this, int|string, \Illuminate\Http\Client\Response|\Illuminate\Http\Client\RequestException|\Illuminate\Http\Client\ConnectionException): void)|null
*/
protected $catchCallback = null;
/**
* The callback to run if all the requests from the batch succeeded.
*
* @var (\Closure($this, array<int|string, \Illuminate\Http\Client\Response>): void)|null
*/
protected $thenCallback = null;
/**
* The callback to run after all the requests from the batch finish.
*
* @var (\Closure($this, array<int|string, \Illuminate\Http\Client\Response>): void)|null
*/
protected $finallyCallback = null;
/**
* If the batch already was sent.
*
* @var bool
*/
protected $inProgress = false;
/**
* The date when the batch was created.
*
* @var \Carbon\CarbonImmutable|null
*/
public $createdAt = null;
/**
* The date when the batch finished.
*
* @var \Carbon\CarbonImmutable|null
*/
public $finishedAt = null;
/**
* The maximum number of concurrent requests.
*
* @var int|null
*/
protected $concurrencyLimit = null;
/**
* Create a new request batch instance.
*/
public function __construct(?Factory $factory = null)
{
$this->factory = $factory ?: new Factory;
$this->handler = Utils::chooseHandler();
$this->createdAt = new CarbonImmutable;
}
/**
* Add a request to the batch with a key.
*
* @param string $key
* @return \Illuminate\Http\Client\PendingRequest
*
* @throws \Illuminate\Http\Client\BatchInProgressException
*/
public function as(string $key)
{
if ($this->inProgress) {
throw new BatchInProgressException();
}
$this->incrementPendingRequests();
return $this->requests[$key] = $this->asyncRequest();
}
/**
* Add a request to the batch with a numeric index.
*
* @return \Illuminate\Http\Client\PendingRequest|\GuzzleHttp\Promise\Promise
*
* @throws \Illuminate\Http\Client\BatchInProgressException
*/
public function newRequest()
{
if ($this->inProgress) {
throw new BatchInProgressException();
}
$this->incrementPendingRequests();
return $this->requests[] = $this->asyncRequest();
}
/**
* Register a callback to run before the first request from the batch runs.
*
* @param (\Closure($this): void) $callback
* @return Batch
*/
public function before(Closure $callback): self
{
$this->beforeCallback = $callback;
return $this;
}
/**
* Register a callback to run after a request from the batch succeeds.
*
* @param (\Closure($this, int|string, \Illuminate\Http\Client\Response): void) $callback
* @return Batch
*/
public function progress(Closure $callback): self
{
$this->progressCallback = $callback;
return $this;
}
/**
* Register a callback to run after a request from the batch fails.
*
* @param (\Closure($this, int|string, \Illuminate\Http\Client\Response|\Illuminate\Http\Client\RequestException|\Illuminate\Http\Client\ConnectionException): void) $callback
* @return Batch
*/
public function catch(Closure $callback): self
{
$this->catchCallback = $callback;
return $this;
}
/**
* Register a callback to run after all the requests from the batch succeed.
*
* @param (\Closure($this, array<int|string, \Illuminate\Http\Client\Response>): void) $callback
* @return Batch
*/
public function then(Closure $callback): self
{
$this->thenCallback = $callback;
return $this;
}
/**
* Register a callback to run after all the requests from the batch finish.
*
* @param (\Closure($this, array<int|string, \Illuminate\Http\Client\Response>): void) $callback
* @return Batch
*/
public function finally(Closure $callback): self
{
$this->finallyCallback = $callback;
return $this;
}
/**
* Set the maximum number of concurrent requests.
*
* @param int $limit
* @return Batch
*/
public function concurrency(int $limit): self
{
$this->concurrencyLimit = $limit;
return $this;
}
/**
* Defer the batch to run in the background after the current task has finished.
*
* @return \Illuminate\Support\Defer\DeferredCallback
*/
public function defer(): DeferredCallback
{
return defer(fn () => $this->send());
}
/**
* Send all of the requests in the batch.
*
* @return array<int|string, \Illuminate\Http\Client\Response|\Illuminate\Http\Client\RequestException>
*/
public function send(): array
{
$this->inProgress = true;
if ($this->beforeCallback !== null) {
call_user_func($this->beforeCallback, $this);
}
$results = [];
if (! empty($this->requests)) {
$eachPromiseOptions = [
'fulfilled' => function ($result, $key) use (&$results) {
$results[$key] = $result;
$this->decrementPendingRequests();
if ($result instanceof Response && $result->successful()) {
if ($this->progressCallback !== null) {
call_user_func($this->progressCallback, $this, $key, $result);
}
return $result;
}
if (
($result instanceof Response && $result->failed()) ||
$result instanceof RequestException ||
$result instanceof ConnectionException
) {
$this->incrementFailedRequests();
if ($this->catchCallback !== null) {
call_user_func($this->catchCallback, $this, $key, $result);
}
}
return $result;
},
'rejected' => function ($reason, $key) {
$this->decrementPendingRequests();
if ($reason instanceof RequestException || $reason instanceof ConnectionException) {
$this->incrementFailedRequests();
if ($this->catchCallback !== null) {
call_user_func($this->catchCallback, $this, $key, $reason);
}
}
return $reason;
},
];
if ($this->concurrencyLimit !== null) {
$eachPromiseOptions['concurrency'] = $this->concurrencyLimit;
}
$promiseGenerator = function () {
foreach ($this->requests as $key => $item) {
$promise = $item instanceof PendingRequest ? $item->getPromise() : $item;
yield $key => $promise instanceof LazyPromise ? $promise->buildPromise() : $promise;
}
};
(new EachPromise($promiseGenerator(), $eachPromiseOptions))
->promise()
->wait();
}
// Before returning the results, we must ensure that the results are sorted
// in the same order as the requests were defined, respecting any custom
// key names that were assigned to this request using the "as" method.
uksort($results, function ($key1, $key2) {
return array_search($key1, array_keys($this->requests), true) <=>
array_search($key2, array_keys($this->requests), true);
});
if (! $this->hasFailures() && $this->thenCallback !== null) {
call_user_func($this->thenCallback, $this, $results);
}
if ($this->finallyCallback !== null) {
call_user_func($this->finallyCallback, $this, $results);
}
$this->finishedAt = new CarbonImmutable;
$this->inProgress = false;
return $results;
}
/**
* Retrieve a new async pending request.
*
* @return \Illuminate\Http\Client\PendingRequest
*/
protected function asyncRequest()
{
return $this->factory->setHandler($this->handler)->async();
}
/**
* Get the total number of requests that have been processed by the batch thus far.
*
* @return non-negative-int
*/
public function processedRequests(): int
{
return $this->totalRequests - $this->pendingRequests;
}
/**
* Determine if the batch has finished executing.
*
* @return bool
*/
public function finished(): bool
{
return ! is_null($this->finishedAt);
}
/**
* Increment the count of total and pending requests in the batch.
*
* @return void
*/
protected function incrementPendingRequests(): void
{
$this->totalRequests++;
$this->pendingRequests++;
}
/**
* Decrement the count of pending requests in the batch.
*
* @return void
*/
protected function decrementPendingRequests(): void
{
$this->pendingRequests--;
}
/**
* Determine if the batch has job failures.
*
* @return bool
*/
public function hasFailures(): bool
{
return $this->failedRequests > 0;
}
/**
* Increment the count of failed requests in the batch.
*
* @return void
*/
protected function incrementFailedRequests(): void
{
$this->failedRequests++;
}
/**
* Get the requests in the batch.
*
* @return array<array-key, \Illuminate\Http\Client\PendingRequest>
*/
public function getRequests(): array
{
return $this->requests;
}
/**
* Add a request to the batch with a numeric index.
*
* @param string $method
* @param array $parameters
* @return \Illuminate\Http\Client\PendingRequest|\GuzzleHttp\Promise\Promise
*
* @throws \Illuminate\Http\Client\BatchInProgressException
*/
public function __call(string $method, array $parameters)
{
return $this->newRequest()->{$method}(...$parameters);
}
}

View File

@@ -0,0 +1,11 @@
<?php
namespace Illuminate\Http\Client;
class BatchInProgressException extends HttpClientException
{
public function __construct()
{
parent::__construct('You cannot add requests to a batch that is already in progress.');
}
}

View File

@@ -0,0 +1,177 @@
<?php
namespace Illuminate\Http\Client\Concerns;
trait DeterminesStatusCode
{
/**
* Determine if the response code was 200 "OK" response.
*
* @return bool
*/
public function ok()
{
return $this->status() === 200;
}
/**
* Determine if the response code was 201 "Created" response.
*
* @return bool
*/
public function created()
{
return $this->status() === 201;
}
/**
* Determine if the response code was 202 "Accepted" response.
*
* @return bool
*/
public function accepted()
{
return $this->status() === 202;
}
/**
* Determine if the response code was the given status code and the body has no content.
*
* @param int $status
* @return bool
*/
public function noContent($status = 204)
{
return $this->status() === $status && $this->body() === '';
}
/**
* Determine if the response code was a 301 "Moved Permanently".
*
* @return bool
*/
public function movedPermanently()
{
return $this->status() === 301;
}
/**
* Determine if the response code was a 302 "Found" response.
*
* @return bool
*/
public function found()
{
return $this->status() === 302;
}
/**
* Determine if the response code was a 304 "Not Modified" response.
*
* @return bool
*/
public function notModified()
{
return $this->status() === 304;
}
/**
* Determine if the response was a 400 "Bad Request" response.
*
* @return bool
*/
public function badRequest()
{
return $this->status() === 400;
}
/**
* Determine if the response was a 401 "Unauthorized" response.
*
* @return bool
*/
public function unauthorized()
{
return $this->status() === 401;
}
/**
* Determine if the response was a 402 "Payment Required" response.
*
* @return bool
*/
public function paymentRequired()
{
return $this->status() === 402;
}
/**
* Determine if the response was a 403 "Forbidden" response.
*
* @return bool
*/
public function forbidden()
{
return $this->status() === 403;
}
/**
* Determine if the response was a 404 "Not Found" response.
*
* @return bool
*/
public function notFound()
{
return $this->status() === 404;
}
/**
* Determine if the response was a 408 "Request Timeout" response.
*
* @return bool
*/
public function requestTimeout()
{
return $this->status() === 408;
}
/**
* Determine if the response was a 409 "Conflict" response.
*
* @return bool
*/
public function conflict()
{
return $this->status() === 409;
}
/**
* Determine if the response was a 422 "Unprocessable Content" response.
*
* @return bool
*/
public function unprocessableContent()
{
return $this->status() === 422;
}
/**
* Determine if the response was a 422 "Unprocessable Content" response.
*
* @return bool
*/
public function unprocessableEntity()
{
return $this->unprocessableContent();
}
/**
* Determine if the response was a 429 "Too Many Requests" response.
*
* @return bool
*/
public function tooManyRequests()
{
return $this->status() === 429;
}
}

View File

@@ -0,0 +1,8 @@
<?php
namespace Illuminate\Http\Client;
class ConnectionException extends HttpClientException
{
//
}

View File

@@ -0,0 +1,35 @@
<?php
namespace Illuminate\Http\Client\Events;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Http\Client\Request;
class ConnectionFailed
{
/**
* The request instance.
*
* @var \Illuminate\Http\Client\Request
*/
public $request;
/**
* The exception instance.
*
* @var \Illuminate\Http\Client\ConnectionException
*/
public $exception;
/**
* Create a new event instance.
*
* @param \Illuminate\Http\Client\Request $request
* @param \Illuminate\Http\Client\ConnectionException $exception
*/
public function __construct(Request $request, ConnectionException $exception)
{
$this->request = $request;
$this->exception = $exception;
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace Illuminate\Http\Client\Events;
use Illuminate\Http\Client\Request;
class RequestSending
{
/**
* The request instance.
*
* @var \Illuminate\Http\Client\Request
*/
public $request;
/**
* Create a new event instance.
*
* @param \Illuminate\Http\Client\Request $request
*/
public function __construct(Request $request)
{
$this->request = $request;
}
}

View File

@@ -0,0 +1,35 @@
<?php
namespace Illuminate\Http\Client\Events;
use Illuminate\Http\Client\Request;
use Illuminate\Http\Client\Response;
class ResponseReceived
{
/**
* The request instance.
*
* @var \Illuminate\Http\Client\Request
*/
public $request;
/**
* The response instance.
*
* @var \Illuminate\Http\Client\Response
*/
public $response;
/**
* Create a new event instance.
*
* @param \Illuminate\Http\Client\Request $request
* @param \Illuminate\Http\Client\Response $response
*/
public function __construct(Request $request, Response $response)
{
$this->request = $request;
$this->response = $response;
}
}

View File

@@ -0,0 +1,557 @@
<?php
namespace Illuminate\Http\Client;
use Closure;
use GuzzleHttp\Exception\ConnectException;
use GuzzleHttp\Middleware;
use GuzzleHttp\Promise\Create;
use GuzzleHttp\Promise\PromiseInterface;
use GuzzleHttp\Psr7\Response as Psr7Response;
use GuzzleHttp\TransferStats;
use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
use Illuminate\Support\Traits\Macroable;
use PHPUnit\Framework\Assert as PHPUnit;
/**
* @mixin \Illuminate\Http\Client\PendingRequest
*/
class Factory
{
use Macroable {
__call as macroCall;
}
/**
* The event dispatcher implementation.
*
* @var \Illuminate\Contracts\Events\Dispatcher|null
*/
protected $dispatcher;
/**
* The middleware to apply to every request.
*
* @var array
*/
protected $globalMiddleware = [];
/**
* The options to apply to every request.
*
* @var \Closure|array
*/
protected $globalOptions = [];
/**
* The stub callables that will handle requests.
*
* @var \Illuminate\Support\Collection
*/
protected $stubCallbacks;
/**
* Indicates if the factory is recording requests and responses.
*
* @var bool
*/
protected $recording = false;
/**
* The recorded response array.
*
* @var list<array{0: \Illuminate\Http\Client\Request, 1: \Illuminate\Http\Client\Response|null}>
*/
protected $recorded = [];
/**
* All created response sequences.
*
* @var list<\Illuminate\Http\Client\ResponseSequence>
*/
protected $responseSequences = [];
/**
* Indicates that an exception should be thrown if any request is not faked.
*
* @var bool
*/
protected $preventStrayRequests = false;
/**
* A list of URL patterns that are allowed to bypass the stray request guard.
*
* @var array<int, string>
*/
protected $allowedStrayRequestUrls = [];
/**
* Create a new factory instance.
*
* @param \Illuminate\Contracts\Events\Dispatcher|null $dispatcher
*/
public function __construct(?Dispatcher $dispatcher = null)
{
$this->dispatcher = $dispatcher;
$this->stubCallbacks = new Collection;
}
/**
* Add middleware to apply to every request.
*
* @param callable $middleware
* @return $this
*/
public function globalMiddleware($middleware)
{
$this->globalMiddleware[] = $middleware;
return $this;
}
/**
* Add request middleware to apply to every request.
*
* @param callable $middleware
* @return $this
*/
public function globalRequestMiddleware($middleware)
{
$this->globalMiddleware[] = Middleware::mapRequest($middleware);
return $this;
}
/**
* Add response middleware to apply to every request.
*
* @param callable $middleware
* @return $this
*/
public function globalResponseMiddleware($middleware)
{
$this->globalMiddleware[] = Middleware::mapResponse($middleware);
return $this;
}
/**
* Set the options to apply to every request.
*
* @param \Closure|array $options
* @return $this
*/
public function globalOptions($options)
{
$this->globalOptions = $options;
return $this;
}
/**
* Create a new response instance for use during stubbing.
*
* @param array|string|null $body
* @param int $status
* @param array $headers
* @return \GuzzleHttp\Promise\PromiseInterface
*/
public static function response($body = null, $status = 200, $headers = [])
{
return Create::promiseFor(
static::psr7Response($body, $status, $headers)
);
}
/**
* Create a new PSR-7 response instance for use during stubbing.
*
* @param array|string|null $body
* @param int $status
* @param array<string, mixed> $headers
* @return \GuzzleHttp\Psr7\Response
*/
public static function psr7Response($body = null, $status = 200, $headers = [])
{
if (is_array($body)) {
$body = json_encode($body);
$headers['Content-Type'] = 'application/json';
}
return new Psr7Response($status, $headers, $body);
}
/**
* Create a new RequestException instance for use during stubbing.
*
* @param array|string|null $body
* @param int $status
* @param array<string, mixed> $headers
* @return \Illuminate\Http\Client\RequestException
*/
public static function failedRequest($body = null, $status = 200, $headers = [])
{
return new RequestException(new Response(static::psr7Response($body, $status, $headers)));
}
/**
* Create a new connection exception for use during stubbing.
*
* @param string|null $message
* @return \Closure(\Illuminate\Http\Client\Request): \GuzzleHttp\Promise\PromiseInterface
*/
public static function failedConnection($message = null)
{
return function ($request) use ($message) {
return Create::rejectionFor(new ConnectException(
$message ?? "cURL error 6: Could not resolve host: {$request->toPsrRequest()->getUri()->getHost()} (see https://curl.haxx.se/libcurl/c/libcurl-errors.html) for {$request->toPsrRequest()->getUri()}.",
$request->toPsrRequest(),
));
};
}
/**
* Get an invokable object that returns a sequence of responses in order for use during stubbing.
*
* @param array $responses
* @return \Illuminate\Http\Client\ResponseSequence
*/
public function sequence(array $responses = [])
{
return $this->responseSequences[] = new ResponseSequence($responses);
}
/**
* Register a stub callable that will intercept requests and be able to return stub responses.
*
* @param callable|array<string, mixed>|null $callback
* @return $this
*/
public function fake($callback = null)
{
$this->record();
$this->recorded = [];
if (is_null($callback)) {
$callback = function () {
return static::response();
};
}
if (is_array($callback)) {
foreach ($callback as $url => $callable) {
$this->stubUrl($url, $callable);
}
return $this;
}
$this->stubCallbacks = $this->stubCallbacks->merge(new Collection([
function ($request, $options) use ($callback) {
$response = $callback;
while ($response instanceof Closure) {
$response = $response($request, $options);
}
if ($response instanceof PromiseInterface) {
$options['on_stats'](new TransferStats(
$request->toPsrRequest(),
$response->wait(),
));
}
return $response;
},
]));
return $this;
}
/**
* Register a response sequence for the given URL pattern.
*
* @param string $url
* @return \Illuminate\Http\Client\ResponseSequence
*/
public function fakeSequence($url = '*')
{
return tap($this->sequence(), function ($sequence) use ($url) {
$this->fake([$url => $sequence]);
});
}
/**
* Stub the given URL using the given callback.
*
* @param string $url
* @param \Illuminate\Http\Client\Response|\GuzzleHttp\Promise\PromiseInterface|callable|int|string|array|\Illuminate\Http\Client\ResponseSequence $callback
* @return $this
*/
public function stubUrl($url, $callback)
{
return $this->fake(function ($request, $options) use ($url, $callback) {
if (! Str::is(Str::start($url, '*'), $request->url())) {
return;
}
if (is_int($callback) && $callback >= 100 && $callback < 600) {
return static::response(status: $callback);
}
if (is_int($callback) || is_string($callback)) {
return static::response($callback);
}
if ($callback instanceof Closure || $callback instanceof ResponseSequence) {
return $callback($request, $options);
}
return $callback;
});
}
/**
* Indicate that an exception should be thrown if any request is not faked.
*
* @param bool $prevent
* @return $this
*/
public function preventStrayRequests($prevent = true)
{
$this->preventStrayRequests = $prevent;
return $this;
}
/**
* Determine if stray requests are being prevented.
*
* @return bool
*/
public function preventingStrayRequests()
{
return $this->preventStrayRequests;
}
/**
* Allow stray, unfaked requests entirely, or optionally allow only specific URLs.
*
* @param array<int, string>|null $only
* @return $this
*/
public function allowStrayRequests(?array $only = null)
{
if (is_null($only)) {
$this->preventStrayRequests(false);
$this->allowedStrayRequestUrls = [];
} else {
$this->allowedStrayRequestUrls = array_values($only);
}
return $this;
}
/**
* Begin recording request / response pairs.
*
* @return $this
*/
public function record()
{
$this->recording = true;
return $this;
}
/**
* Record a request response pair.
*
* @param \Illuminate\Http\Client\Request $request
* @param \Illuminate\Http\Client\Response|null $response
* @return void
*/
public function recordRequestResponsePair($request, $response)
{
if ($this->recording) {
$this->recorded[] = [$request, $response];
}
}
/**
* Assert that a request / response pair was recorded matching a given truth test.
*
* @param callable|(\Closure(\Illuminate\Http\Client\Request, \Illuminate\Http\Client\Response|null): bool) $callback
* @return void
*/
public function assertSent($callback)
{
PHPUnit::assertTrue(
$this->recorded($callback)->count() > 0,
'An expected request was not recorded.'
);
}
/**
* Assert that the given request was sent in the given order.
*
* @param list<string|(\Closure(\Illuminate\Http\Client\Request, \Illuminate\Http\Client\Response|null): bool)|callable> $callbacks
* @return void
*/
public function assertSentInOrder($callbacks)
{
$this->assertSentCount(count($callbacks));
foreach ($callbacks as $index => $url) {
$callback = is_callable($url) ? $url : function ($request) use ($url) {
return $request->url() == $url;
};
PHPUnit::assertTrue($callback(
$this->recorded[$index][0],
$this->recorded[$index][1]
), 'An expected request (#'.($index + 1).') was not recorded.');
}
}
/**
* Assert that a request / response pair was not recorded matching a given truth test.
*
* @param callable|(\Closure(\Illuminate\Http\Client\Request, \Illuminate\Http\Client\Response|null): bool) $callback
* @return void
*/
public function assertNotSent($callback)
{
PHPUnit::assertFalse(
$this->recorded($callback)->count() > 0,
'Unexpected request was recorded.'
);
}
/**
* Assert that no request / response pair was recorded.
*
* @return void
*/
public function assertNothingSent()
{
PHPUnit::assertEmpty(
$this->recorded,
'Requests were recorded.'
);
}
/**
* Assert how many requests have been recorded.
*
* @param int $count
* @return void
*/
public function assertSentCount($count)
{
PHPUnit::assertCount($count, $this->recorded);
}
/**
* Assert that every created response sequence is empty.
*
* @return void
*/
public function assertSequencesAreEmpty()
{
foreach ($this->responseSequences as $responseSequence) {
PHPUnit::assertTrue(
$responseSequence->isEmpty(),
'Not all response sequences are empty.'
);
}
}
/**
* Get a collection of the request / response pairs matching the given truth test.
*
* @param (\Closure(\Illuminate\Http\Client\Request, \Illuminate\Http\Client\Response|null): bool)|callable $callback
* @return \Illuminate\Support\Collection<int, array{0: \Illuminate\Http\Client\Request, 1: \Illuminate\Http\Client\Response|null}>
*/
public function recorded($callback = null)
{
if (empty($this->recorded)) {
return new Collection;
}
$collect = new Collection($this->recorded);
if ($callback) {
return $collect->filter(fn ($pair) => $callback($pair[0], $pair[1]));
}
return $collect;
}
/**
* Create a new pending request instance for this factory.
*
* @return \Illuminate\Http\Client\PendingRequest
*/
public function createPendingRequest()
{
return tap($this->newPendingRequest(), function ($request) {
$request
->stub($this->stubCallbacks)
->preventStrayRequests($this->preventStrayRequests)
->allowStrayRequests($this->allowedStrayRequestUrls);
});
}
/**
* Instantiate a new pending request instance for this factory.
*
* @return \Illuminate\Http\Client\PendingRequest
*/
protected function newPendingRequest()
{
return (new PendingRequest($this, $this->globalMiddleware))->withOptions(value($this->globalOptions));
}
/**
* Get the current event dispatcher implementation.
*
* @return \Illuminate\Contracts\Events\Dispatcher|null
*/
public function getDispatcher()
{
return $this->dispatcher;
}
/**
* Get the array of global middleware.
*
* @return array
*/
public function getGlobalMiddleware()
{
return $this->globalMiddleware;
}
/**
* Execute a method against a new pending request instance.
*
* @param string $method
* @param array $parameters
* @return mixed
*/
public function __call($method, $parameters)
{
if (static::hasMacro($method)) {
return $this->macroCall($method, $parameters);
}
return $this->createPendingRequest()->{$method}(...$parameters);
}
}

View File

@@ -0,0 +1,10 @@
<?php
namespace Illuminate\Http\Client;
use Exception;
class HttpClientException extends Exception
{
//
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,96 @@
<?php
namespace Illuminate\Http\Client;
use GuzzleHttp\Utils;
/**
* @mixin \Illuminate\Http\Client\Factory
*/
class Pool
{
/**
* The factory instance.
*
* @var \Illuminate\Http\Client\Factory
*/
protected $factory;
/**
* The handler function for the Guzzle client.
*
* @var callable
*/
protected $handler;
/**
* The pool of requests.
*
* @var array<array-key, \Illuminate\Http\Client\PendingRequest>
*/
protected $pool = [];
/**
* Create a new requests pool.
*
* @param \Illuminate\Http\Client\Factory|null $factory
*/
public function __construct(?Factory $factory = null)
{
$this->factory = $factory ?: new Factory();
$this->handler = Utils::chooseHandler();
}
/**
* Add a request to the pool with a numeric index.
*
* @return \Illuminate\Http\Client\PendingRequest|\GuzzleHttp\Promise\Promise
*/
public function newRequest()
{
return $this->pool[] = $this->asyncRequest();
}
/**
* Add a request to the pool with a key.
*
* @param string $key
* @return \Illuminate\Http\Client\PendingRequest
*/
public function as(string $key)
{
return $this->pool[$key] = $this->asyncRequest();
}
/**
* Retrieve a new async pending request.
*
* @return \Illuminate\Http\Client\PendingRequest
*/
protected function asyncRequest()
{
return $this->factory->setHandler($this->handler)->async();
}
/**
* Retrieve the requests in the pool.
*
* @return array<array-key, \Illuminate\Http\Client\PendingRequest>
*/
public function getRequests()
{
return $this->pool;
}
/**
* Add a request to the pool with a numeric index and forward the method call to the request.
*
* @param string $method
* @param array $parameters
* @return \Illuminate\Http\Client\PendingRequest|\GuzzleHttp\Promise\Promise
*/
public function __call($method, $parameters)
{
return $this->newRequest()->{$method}(...$parameters);
}
}

View File

@@ -0,0 +1,95 @@
<?php
namespace Illuminate\Http\Client\Promises;
use GuzzleHttp\Promise\PromiseInterface;
use Illuminate\Support\Traits\ForwardsCalls;
/**
* A decorated Promise which allows for chaining callbacks.
*/
class FluentPromise implements PromiseInterface
{
use ForwardsCalls;
/**
* Create a new fluent promise instance.
*
* @param \GuzzleHttp\Promise\PromiseInterface $guzzlePromise
*/
public function __construct(protected PromiseInterface $guzzlePromise)
{
}
#[\Override]
public function then(?callable $onFulfilled = null, ?callable $onRejected = null): PromiseInterface
{
return $this->__call('then', [$onFulfilled, $onRejected]);
}
#[\Override]
public function otherwise(callable $onRejected): PromiseInterface
{
return $this->__call('otherwise', [$onRejected]);
}
#[\Override]
public function resolve($value): void
{
$this->guzzlePromise->resolve($value);
}
#[\Override]
public function reject($reason): void
{
$this->guzzlePromise->reject($reason);
}
#[\Override]
public function cancel(): void
{
$this->guzzlePromise->cancel();
}
#[\Override]
public function wait(bool $unwrap = true)
{
return $this->__call('wait', [$unwrap]);
}
#[\Override]
public function getState(): string
{
return $this->guzzlePromise->getState();
}
/**
* Get the underlying Guzzle promise.
*
* @return \GuzzleHttp\Promise\PromiseInterface
*/
public function getGuzzlePromise(): PromiseInterface
{
return $this->guzzlePromise;
}
/**
* Proxy requests to the underlying promise interface and update the local promise.
*
* @param string $method
* @param array $parameters
* @return mixed
*/
public function __call($method, $parameters)
{
$result = $this->forwardCallTo($this->guzzlePromise, $method, $parameters);
if (! $result instanceof PromiseInterface) {
return $result;
}
$this->guzzlePromise = $result;
return $this;
}
}

View File

@@ -0,0 +1,129 @@
<?php
namespace Illuminate\Http\Client\Promises;
use Closure;
use GuzzleHttp\Promise\PromiseInterface;
use RuntimeException;
class LazyPromise implements PromiseInterface
{
/**
* The callbacks to execute after the Guzzle Promise has been built.
*
* @var list<callable>
*/
protected array $pending = [];
/**
* The promise built by the creator.
*
* @var \GuzzleHttp\Promise\PromiseInterface
*/
protected PromiseInterface $guzzlePromise;
/**
* Create a new lazy promise instance.
*
* @param (\Closure(): \GuzzleHttp\Promise\PromiseInterface) $promiseBuilder The callback to build a new PromiseInterface.
*/
public function __construct(protected Closure $promiseBuilder)
{
}
/**
* Build the promise from the promise builder.
*
* @return \GuzzleHttp\Promise\PromiseInterface
*
* @throws \RuntimeException If the promise has already been built
*/
public function buildPromise(): PromiseInterface
{
if (! $this->promiseNeedsBuilt()) {
throw new RuntimeException('Promise already built');
}
$this->guzzlePromise = call_user_func($this->promiseBuilder);
foreach ($this->pending as $pendingCallback) {
$pendingCallback($this->guzzlePromise);
}
$this->pending = [];
return $this->guzzlePromise;
}
#[\Override]
public function then(?callable $onFulfilled = null, ?callable $onRejected = null): PromiseInterface
{
if ($this->promiseNeedsBuilt()) {
$this->pending[] = static fn (PromiseInterface $promise) => $promise->then($onFulfilled, $onRejected);
return $this;
}
return $this->guzzlePromise->then($onFulfilled, $onRejected);
}
#[\Override]
public function otherwise(callable $onRejected): PromiseInterface
{
if ($this->promiseNeedsBuilt()) {
$this->pending[] = static fn (PromiseInterface $promise) => $promise->otherwise($onRejected);
return $this;
}
return $this->guzzlePromise->otherwise($onRejected);
}
#[\Override]
public function getState(): string
{
if ($this->promiseNeedsBuilt()) {
return PromiseInterface::PENDING;
}
return $this->guzzlePromise->getState();
}
#[\Override]
public function resolve($value): void
{
throw new \LogicException('Cannot resolve a lazy promise.');
}
#[\Override]
public function reject($reason): void
{
throw new \LogicException('Cannot reject a lazy promise.');
}
#[\Override]
public function cancel(): void
{
throw new \LogicException('Cannot cancel a lazy promise.');
}
#[\Override]
public function wait(bool $unwrap = true)
{
if ($this->promiseNeedsBuilt()) {
$this->buildPromise();
}
return $this->guzzlePromise->wait($unwrap);
}
/**
* Determine if the promise has been created from the promise builder.
*
* @return bool
*/
public function promiseNeedsBuilt(): bool
{
return ! isset($this->guzzlePromise);
}
}

View File

@@ -0,0 +1,335 @@
<?php
namespace Illuminate\Http\Client;
use ArrayAccess;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Illuminate\Support\Traits\Macroable;
use LogicException;
class Request implements ArrayAccess
{
use Macroable;
/**
* The underlying PSR request.
*
* @var \Psr\Http\Message\RequestInterface
*/
protected $request;
/**
* The decoded payload for the request.
*
* @var array
*/
protected $data;
/**
* The attribute data passed when building the PendingRequest.
*
* @var array<array-key, mixed>
*/
protected $attributes = [];
/**
* Create a new request instance.
*
* @param \Psr\Http\Message\RequestInterface $request
*/
public function __construct($request)
{
$this->request = $request;
}
/**
* Get the request method.
*
* @return string
*/
public function method()
{
return $this->request->getMethod();
}
/**
* Get the URL of the request.
*
* @return string
*/
public function url()
{
return (string) $this->request->getUri();
}
/**
* Determine if the request has a given header.
*
* @param string $key
* @param mixed $value
* @return bool
*/
public function hasHeader($key, $value = null)
{
if (is_null($value)) {
return ! empty($this->request->getHeaders()[$key]);
}
$headers = $this->headers();
if (! Arr::has($headers, $key)) {
return false;
}
$value = is_array($value) ? $value : [$value];
return empty(array_diff($value, $headers[$key]));
}
/**
* Determine if the request has the given headers.
*
* @param array|string $headers
* @return bool
*/
public function hasHeaders($headers)
{
if (is_string($headers)) {
$headers = [$headers => null];
}
foreach ($headers as $key => $value) {
if (! $this->hasHeader($key, $value)) {
return false;
}
}
return true;
}
/**
* Get the values for the header with the given name.
*
* @param string $key
* @return array
*/
public function header($key)
{
return Arr::get($this->headers(), $key, []);
}
/**
* Get the request headers.
*
* @return array
*/
public function headers()
{
return $this->request->getHeaders();
}
/**
* Get the body of the request.
*
* @return string
*/
public function body()
{
return (string) $this->request->getBody();
}
/**
* Determine if the request contains the given file.
*
* @param string $name
* @param string|null $value
* @param string|null $filename
* @return bool
*/
public function hasFile($name, $value = null, $filename = null)
{
if (! $this->isMultipart()) {
return false;
}
return (new Collection($this->data))->reject(function ($file) use ($name, $value, $filename) {
return $file['name'] != $name ||
($value && $file['contents'] != $value) ||
($filename && $file['filename'] != $filename);
})->count() > 0;
}
/**
* Get the request's data (form parameters or JSON).
*
* @return array
*/
public function data()
{
if ($this->isForm()) {
return $this->parameters();
} elseif ($this->isJson()) {
return $this->json();
}
return $this->data ?? [];
}
/**
* Get the request's form parameters.
*
* @return array
*/
protected function parameters()
{
if (! $this->data) {
parse_str($this->body(), $parameters);
$this->data = $parameters;
}
return $this->data;
}
/**
* Get the JSON decoded body of the request.
*
* @return array
*/
protected function json()
{
if (! $this->data) {
$this->data = json_decode($this->body(), true) ?? [];
}
return $this->data;
}
/**
* Determine if the request is simple form data.
*
* @return bool
*/
public function isForm()
{
return $this->hasHeader('Content-Type', 'application/x-www-form-urlencoded');
}
/**
* Determine if the request is JSON.
*
* @return bool
*/
public function isJson()
{
return $this->hasHeader('Content-Type') &&
str_contains($this->header('Content-Type')[0], 'json');
}
/**
* Determine if the request is multipart.
*
* @return bool
*/
public function isMultipart()
{
return $this->hasHeader('Content-Type') &&
str_contains($this->header('Content-Type')[0], 'multipart');
}
/**
* Set the decoded data on the request.
*
* @param array $data
* @return $this
*/
public function withData(array $data)
{
$this->data = $data;
return $this;
}
/**
* Get the attribute data from the request.
*
* @return array<array-key, mixed>
*/
public function attributes()
{
return $this->attributes;
}
/**
* Set the request's attribute data.
*
* @param array<array-key, mixed> $attributes
* @return $this
*/
public function setRequestAttributes($attributes)
{
$this->attributes = $attributes;
return $this;
}
/**
* Get the underlying PSR compliant request instance.
*
* @return \Psr\Http\Message\RequestInterface
*/
public function toPsrRequest()
{
return $this->request;
}
/**
* Determine if the given offset exists.
*
* @param string $offset
* @return bool
*/
public function offsetExists($offset): bool
{
return isset($this->data()[$offset]);
}
/**
* Get the value for a given offset.
*
* @param string $offset
* @return mixed
*/
public function offsetGet($offset): mixed
{
return $this->data()[$offset];
}
/**
* Set the value at the given offset.
*
* @param string $offset
* @param mixed $value
* @return void
*
* @throws \LogicException
*/
public function offsetSet($offset, $value): void
{
throw new LogicException('Request data may not be mutated using array access.');
}
/**
* Unset the value at the given offset.
*
* @param string $offset
* @return void
*
* @throws \LogicException
*/
public function offsetUnset($offset): void
{
throw new LogicException('Request data may not be mutated using array access.');
}
}

View File

@@ -0,0 +1,117 @@
<?php
namespace Illuminate\Http\Client;
use GuzzleHttp\Psr7\Message;
class RequestException extends HttpClientException
{
/**
* The response instance.
*
* @var \Illuminate\Http\Client\Response
*/
public $response;
/**
* The current truncation length for the exception message.
*
* @var int|false|null
*/
public $truncateExceptionsAt;
/**
* The global truncation length for the exception message.
*
* @var int|false
*/
public static $truncateAt = 120;
/**
* Whether the response has been summarized in the message.
*
* @var bool
*/
public $hasBeenSummarized = false;
/**
* Create a new exception instance.
*
* @param \Illuminate\Http\Client\Response $response
* @param int|false|null $truncateExceptionsAt
*/
public function __construct(Response $response, $truncateExceptionsAt = null)
{
parent::__construct($this->prepareMessage($response), $response->status());
$this->truncateExceptionsAt = $truncateExceptionsAt;
$this->response = $response;
}
/**
* Enable truncation of request exception messages.
*
* @return void
*/
public static function truncate()
{
static::$truncateAt = 120;
}
/**
* Set the truncation length for request exception messages.
*
* @param int $length
* @return void
*/
public static function truncateAt(int $length)
{
static::$truncateAt = $length;
}
/**
* Disable truncation of request exception messages.
*
* @return void
*/
public static function dontTruncate()
{
static::$truncateAt = false;
}
/**
* Prepare the exception message.
*
* @return bool
*/
public function report()
{
if (! $this->hasBeenSummarized) {
$this->message = $this->prepareMessage($this->response);
$this->hasBeenSummarized = true;
}
return false;
}
/**
* Prepare the exception message.
*
* @param \Illuminate\Http\Client\Response $response
* @return string
*/
protected function prepareMessage(Response $response)
{
$message = "HTTP request returned status code {$response->status()}";
$truncateExceptionsAt = $this->truncateExceptionsAt ?? static::$truncateAt;
$summary = is_int($truncateExceptionsAt)
? Message::bodySummary($response->toPsrResponse(), $truncateExceptionsAt)
: Message::toString($response->toPsrResponse());
return is_null($summary) ? $message : $message.":\n{$summary}\n";
}
}

View File

@@ -0,0 +1,610 @@
<?php
namespace Illuminate\Http\Client;
use ArrayAccess;
use GuzzleHttp\Psr7\StreamWrapper;
use Illuminate\Support\Collection;
use Illuminate\Support\Fluent;
use Illuminate\Support\Traits\Macroable;
use Illuminate\Support\Traits\Tappable;
use LogicException;
use Stringable;
/**
* @mixin \Psr\Http\Message\ResponseInterface
*/
class Response implements ArrayAccess, Stringable
{
use Concerns\DeterminesStatusCode, Tappable, Macroable {
__call as macroCall;
}
/**
* The underlying PSR response.
*
* @var \Psr\Http\Message\ResponseInterface
*/
protected $response;
/**
* The decoded JSON response.
*
* @var array
*/
protected $decoded;
/**
* The flags that were used when decoding the JSON response.
*
* @var int-mask<JSON_BIGINT_AS_STRING, JSON_INVALID_UTF8_IGNORE, JSON_INVALID_UTF8_SUBSTITUTE, JSON_OBJECT_AS_ARRAY, JSON_THROW_ON_ERROR>
*/
protected int $decodingFlags;
/**
* The request cookies.
*
* @var \GuzzleHttp\Cookie\CookieJar
*/
public $cookies;
/**
* The transfer stats for the request.
*
* @var \GuzzleHttp\TransferStats|null
*/
public $transferStats;
/**
* The length at which request exceptions will be truncated.
*
* @var int<1, max>|false|null
*/
protected $truncateExceptionsAt = null;
/**
* The flags passed to `json_decode` by default.
*
* @var int-mask<JSON_BIGINT_AS_STRING, JSON_INVALID_UTF8_IGNORE, JSON_INVALID_UTF8_SUBSTITUTE, JSON_OBJECT_AS_ARRAY, JSON_THROW_ON_ERROR>
*/
public static int $defaultJsonDecodingFlags = 0;
/**
* Create a new response instance.
*
* @param \Psr\Http\Message\MessageInterface $response
*/
public function __construct($response)
{
$this->response = $response;
}
/**
* Get the body of the response.
*
* @return string
*/
public function body()
{
return (string) $this->response->getBody();
}
/**
* Get the JSON decoded body of the response as an array or scalar value.
*
* @param string|null $key
* @param mixed $default
* @param int-mask<JSON_BIGINT_AS_STRING, JSON_INVALID_UTF8_IGNORE, JSON_INVALID_UTF8_SUBSTITUTE, JSON_OBJECT_AS_ARRAY, JSON_THROW_ON_ERROR>|null $flags
* @return mixed
*/
public function json($key = null, $default = null, $flags = null)
{
$flags = $flags ?? self::$defaultJsonDecodingFlags;
if (! $this->decoded || (isset($this->decodingFlags) && $this->decodingFlags !== $flags)) {
$this->decoded = json_decode(
$this->body(), true, flags: $flags
);
$this->decodingFlags = $flags;
}
if (is_null($key)) {
return $this->decoded;
}
return data_get($this->decoded, $key, $default);
}
/**
* Get the JSON decoded body of the response as an object.
*
* @param int-mask<JSON_BIGINT_AS_STRING, JSON_INVALID_UTF8_IGNORE, JSON_INVALID_UTF8_SUBSTITUTE, JSON_OBJECT_AS_ARRAY, JSON_THROW_ON_ERROR>|null $flags
* @return object|null
*/
public function object($flags = null)
{
return json_decode($this->body(), false, flags: $flags ?? self::$defaultJsonDecodingFlags);
}
/**
* Get the JSON decoded body of the response as a collection.
*
* @param string|null $key
* @param int-mask<JSON_BIGINT_AS_STRING, JSON_INVALID_UTF8_IGNORE, JSON_INVALID_UTF8_SUBSTITUTE, JSON_OBJECT_AS_ARRAY, JSON_THROW_ON_ERROR>|null $flags
* @return \Illuminate\Support\Collection
*/
public function collect($key = null, $flags = null)
{
return new Collection($this->json($key, flags: $flags));
}
/**
* Get the JSON decoded body of the response as a fluent object.
*
* @param string|null $key
* @param int-mask<JSON_BIGINT_AS_STRING, JSON_INVALID_UTF8_IGNORE, JSON_INVALID_UTF8_SUBSTITUTE, JSON_OBJECT_AS_ARRAY, JSON_THROW_ON_ERROR>|null $flags
* @return \Illuminate\Support\Fluent
*/
public function fluent($key = null, $flags = null)
{
return new Fluent((array) $this->json($key, flags: $flags));
}
/**
* Get the body of the response as a PHP resource.
*
* @return resource
*
* @throws \InvalidArgumentException
*/
public function resource()
{
return StreamWrapper::getResource($this->response->getBody());
}
/**
* Get a header from the response.
*
* @param string $header
* @return string
*/
public function header(string $header)
{
return $this->response->getHeaderLine($header);
}
/**
* Get the headers from the response.
*
* @return array
*/
public function headers()
{
return $this->response->getHeaders();
}
/**
* Get the status code of the response.
*
* @return int
*/
public function status()
{
return (int) $this->response->getStatusCode();
}
/**
* Get the reason phrase of the response.
*
* @return string
*/
public function reason()
{
return $this->response->getReasonPhrase();
}
/**
* Get the effective URI of the response.
*
* @return \Psr\Http\Message\UriInterface|null
*/
public function effectiveUri()
{
return $this->transferStats?->getEffectiveUri();
}
/**
* Determine if the request was successful.
*
* @return bool
*/
public function successful()
{
return $this->status() >= 200 && $this->status() < 300;
}
/**
* Determine if the response was a redirect.
*
* @return bool
*/
public function redirect()
{
return $this->status() >= 300 && $this->status() < 400;
}
/**
* Determine if the response indicates a client or server error occurred.
*
* @return bool
*/
public function failed()
{
return $this->serverError() || $this->clientError();
}
/**
* Determine if the response indicates a client error occurred.
*
* @return bool
*/
public function clientError()
{
return $this->status() >= 400 && $this->status() < 500;
}
/**
* Determine if the response indicates a server error occurred.
*
* @return bool
*/
public function serverError()
{
return $this->status() >= 500;
}
/**
* Execute the given callback if there was a server or client error.
*
* @param callable|(\Closure(\Illuminate\Http\Client\Response): mixed) $callback
* @return $this
*/
public function onError(callable $callback)
{
if ($this->failed()) {
$callback($this);
}
return $this;
}
/**
* Get the response cookies.
*
* @return \GuzzleHttp\Cookie\CookieJar
*/
public function cookies()
{
return $this->cookies;
}
/**
* Get the handler stats of the response.
*
* @return array
*/
public function handlerStats()
{
return $this->transferStats?->getHandlerStats() ?? [];
}
/**
* Close the stream and any underlying resources.
*
* @return $this
*/
public function close()
{
$this->response->getBody()->close();
return $this;
}
/**
* Get the underlying PSR response for the response.
*
* @return \Psr\Http\Message\ResponseInterface
*/
public function toPsrResponse()
{
return $this->response;
}
/**
* Create an exception if a server or client error occurred.
*
* @return \Illuminate\Http\Client\RequestException|null
*/
public function toException()
{
if ($this->failed()) {
return new RequestException($this, $this->truncateExceptionsAt);
}
}
/**
* Throw an exception if a server or client error occurred.
*
* @return $this
*
* @throws \Illuminate\Http\Client\RequestException
*/
public function throw()
{
$callback = func_get_args()[0] ?? null;
if ($this->failed()) {
throw tap($this->toException(), function ($exception) use ($callback) {
if ($callback && is_callable($callback)) {
$callback($this, $exception);
}
});
}
return $this;
}
/**
* Throw an exception if a server or client error occurred and the given condition evaluates to true.
*
* @param \Closure|bool $condition
* @return $this
*
* @throws \Illuminate\Http\Client\RequestException
*/
public function throwIf($condition)
{
return value($condition, $this) ? $this->throw(func_get_args()[1] ?? null) : $this;
}
/**
* Throw an exception if a server or client error occurred and the given condition evaluates to false.
*
* @param \Closure|bool $condition
* @return $this
*
* @throws \Illuminate\Http\Client\RequestException
*/
public function throwUnless($condition)
{
return $this->throwIf(! $condition);
}
/**
* Throw an exception if the response status code matches the given code.
*
* @param int|(\Closure(int, \Illuminate\Http\Client\Response): bool)|callable $statusCode
* @return $this
*
* @throws \Illuminate\Http\Client\RequestException
*/
public function throwIfStatus($statusCode)
{
if (is_callable($statusCode) &&
$statusCode($this->status(), $this)) {
throw new RequestException($this, $this->truncateExceptionsAt);
}
return $this->status() === $statusCode ? throw new RequestException($this, $this->truncateExceptionsAt) : $this;
}
/**
* Throw an exception unless the response status code matches the given code.
*
* @param int|(\Closure(int, \Illuminate\Http\Client\Response): bool)|callable $statusCode
* @return $this
*
* @throws \Illuminate\Http\Client\RequestException
*/
public function throwUnlessStatus($statusCode)
{
if (is_callable($statusCode)) {
return $statusCode($this->status(), $this) ? $this : throw new RequestException($this, $this->truncateExceptionsAt);
}
return $this->status() === $statusCode ? $this : throw new RequestException($this, $this->truncateExceptionsAt);
}
/**
* Throw an exception if the response status code is a 4xx level code.
*
* @return $this
*
* @throws \Illuminate\Http\Client\RequestException
*/
public function throwIfClientError()
{
return $this->clientError() ? $this->throw() : $this;
}
/**
* Throw an exception if the response status code is a 5xx level code.
*
* @return $this
*
* @throws \Illuminate\Http\Client\RequestException
*/
public function throwIfServerError()
{
return $this->serverError() ? $this->throw() : $this;
}
/**
* Indicate that request exceptions should be truncated to the given length.
*
* @param int<1, max> $length
* @return $this
*/
public function truncateExceptionsAt(int $length)
{
$this->truncateExceptionsAt = $length;
return $this;
}
/**
* Indicate that request exceptions should not be truncated.
*
* @return $this
*/
public function dontTruncateExceptions()
{
$this->truncateExceptionsAt = false;
return $this;
}
/**
* Dump the content from the response.
*
* @param string|null $key
* @return $this
*/
public function dump($key = null)
{
$content = $this->body();
$json = json_decode($content);
if (json_last_error() === JSON_ERROR_NONE) {
$content = $json;
}
if (! is_null($key)) {
dump(data_get($content, $key));
} else {
dump($content);
}
return $this;
}
/**
* Dump the content from the response and end the script.
*
* @param string|null $key
* @return never
*/
public function dd($key = null)
{
$this->dump($key);
exit(1);
}
/**
* Dump the headers from the response.
*
* @return $this
*/
public function dumpHeaders()
{
dump($this->headers());
return $this;
}
/**
* Dump the headers from the response and end the script.
*
* @return never
*/
public function ddHeaders()
{
$this->dumpHeaders();
exit(1);
}
/**
* Determine if the given offset exists.
*
* @param string $offset
* @return bool
*/
public function offsetExists($offset): bool
{
return isset($this->json()[$offset]);
}
/**
* Get the value for a given offset.
*
* @param string $offset
* @return mixed
*/
public function offsetGet($offset): mixed
{
return $this->json()[$offset];
}
/**
* Set the value at the given offset.
*
* @param string $offset
* @param mixed $value
* @return void
*
* @throws \LogicException
*/
public function offsetSet($offset, $value): void
{
throw new LogicException('Response data may not be mutated using array access.');
}
/**
* Unset the value at the given offset.
*
* @param string $offset
* @return void
*
* @throws \LogicException
*/
public function offsetUnset($offset): void
{
throw new LogicException('Response data may not be mutated using array access.');
}
/**
* Get the body of the response.
*
* @return string
*/
public function __toString()
{
return $this->body();
}
/**
* Dynamically proxy other methods to the underlying response.
*
* @param string $method
* @param array $parameters
* @return mixed
*/
public function __call($method, $parameters)
{
return static::hasMacro($method)
? $this->macroCall($method, $parameters)
: $this->response->{$method}(...$parameters);
}
/**
* Flush the global state of the Response.
*/
public static function flushState(): void
{
self::$defaultJsonDecodingFlags = 0;
}
}

View File

@@ -0,0 +1,172 @@
<?php
namespace Illuminate\Http\Client;
use Closure;
use Illuminate\Support\Traits\Macroable;
use OutOfBoundsException;
class ResponseSequence
{
use Macroable;
/**
* The responses in the sequence.
*
* @var array
*/
protected $responses;
/**
* Indicates that invoking this sequence when it is empty should throw an exception.
*
* @var bool
*/
protected $failWhenEmpty = true;
/**
* The response that should be returned when the sequence is empty.
*
* @var \GuzzleHttp\Promise\PromiseInterface
*/
protected $emptyResponse;
/**
* Create a new response sequence.
*
* @param array $responses
*/
public function __construct(array $responses)
{
$this->responses = $responses;
}
/**
* Push a response to the sequence.
*
* @param string|array|null $body
* @param int $status
* @param array $headers
* @return $this
*/
public function push($body = null, int $status = 200, array $headers = [])
{
return $this->pushResponse(
Factory::response($body, $status, $headers)
);
}
/**
* Push a response with the given status code to the sequence.
*
* @param int $status
* @param array $headers
* @return $this
*/
public function pushStatus(int $status, array $headers = [])
{
return $this->pushResponse(
Factory::response('', $status, $headers)
);
}
/**
* Push a response with the contents of a file as the body to the sequence.
*
* @param string $filePath
* @param int $status
* @param array $headers
* @return $this
*/
public function pushFile(string $filePath, int $status = 200, array $headers = [])
{
$string = file_get_contents($filePath);
return $this->pushResponse(
Factory::response($string, $status, $headers)
);
}
/**
* Push a connection exception to the sequence.
*
* @param string|null $message
* @return $this
*/
public function pushFailedConnection($message = null)
{
return $this->pushResponse(
Factory::failedConnection($message)
);
}
/**
* Push a response to the sequence.
*
* @param mixed $response
* @return $this
*/
public function pushResponse($response)
{
$this->responses[] = $response;
return $this;
}
/**
* Make the sequence return a default response when it is empty.
*
* @param \GuzzleHttp\Promise\PromiseInterface|\Closure $response
* @return $this
*/
public function whenEmpty($response)
{
$this->failWhenEmpty = false;
$this->emptyResponse = $response;
return $this;
}
/**
* Make the sequence return a default response when it is empty.
*
* @return $this
*/
public function dontFailWhenEmpty()
{
return $this->whenEmpty(Factory::response());
}
/**
* Indicate that this sequence has depleted all of its responses.
*
* @return bool
*/
public function isEmpty()
{
return count($this->responses) === 0;
}
/**
* Get the next response in the sequence.
*
* @param \Illuminate\Http\Client\Request $request
* @return mixed
*
* @throws \OutOfBoundsException
*/
public function __invoke($request)
{
if ($this->failWhenEmpty && $this->isEmpty()) {
throw new OutOfBoundsException('A request was made, but the response sequence is empty.');
}
if (! $this->failWhenEmpty && $this->isEmpty()) {
return value($this->emptyResponse ?? Factory::response());
}
$response = array_shift($this->responses);
return $response instanceof Closure ? $response($request) : $response;
}
}

View File

@@ -0,0 +1,13 @@
<?php
namespace Illuminate\Http\Client;
use RuntimeException;
class StrayRequestException extends RuntimeException
{
public function __construct(string $uri)
{
parent::__construct('Attempted request to ['.$uri.'] without a matching fake.');
}
}

View File

@@ -0,0 +1,67 @@
<?php
namespace Illuminate\Http\Concerns;
use Illuminate\Support\Collection;
trait CanBePrecognitive
{
/**
* Filter the given array of rules into an array of rules that are included in precognitive headers.
*
* @param array $rules
* @return array
*/
public function filterPrecognitiveRules($rules)
{
if (! $this->headers->has('Precognition-Validate-Only')) {
return $rules;
}
$validateOnly = explode(',', $this->header('Precognition-Validate-Only'));
return (new Collection($rules))
->filter(fn ($rule, $attribute) => $this->shouldValidatePrecognitiveAttribute($attribute, $validateOnly))
->all();
}
/**
* Determine if the given attribute should be validated.
*
* @param string $attribute
* @param array $validateOnly
* @return bool
*/
protected function shouldValidatePrecognitiveAttribute($attribute, $validateOnly)
{
foreach ($validateOnly as $pattern) {
$regex = '/^'.str_replace('\*', '[^.]+', preg_quote($pattern, '/')).'$/';
if (preg_match($regex, $attribute)) {
return true;
}
}
return false;
}
/**
* Determine if the request is attempting to be precognitive.
*
* @return bool
*/
public function isAttemptingPrecognition()
{
return $this->header('Precognition') === 'true';
}
/**
* Determine if the request is precognitive.
*
* @return bool
*/
public function isPrecognitive()
{
return $this->attributes->get('precognitive', false);
}
}

View File

@@ -0,0 +1,187 @@
<?php
namespace Illuminate\Http\Concerns;
use Illuminate\Support\Str;
trait InteractsWithContentTypes
{
/**
* Determine if the request is sending JSON.
*
* @return bool
*/
public function isJson()
{
return Str::contains($this->header('CONTENT_TYPE') ?? '', ['/json', '+json']);
}
/**
* Determine if the current request probably expects a JSON response.
*
* @return bool
*/
public function expectsJson()
{
return ($this->ajax() && ! $this->pjax() && $this->acceptsAnyContentType()) || $this->wantsJson();
}
/**
* Determine if the current request is asking for JSON.
*
* @return bool
*/
public function wantsJson()
{
$acceptable = $this->getAcceptableContentTypes();
return isset($acceptable[0]) && Str::contains(strtolower($acceptable[0]), ['/json', '+json']);
}
/**
* Determines whether the current requests accepts a given content type.
*
* @param string|array $contentTypes
* @return bool
*/
public function accepts($contentTypes)
{
$accepts = $this->getAcceptableContentTypes();
if (count($accepts) === 0) {
return true;
}
$types = (array) $contentTypes;
foreach ($accepts as $accept) {
if ($accept && $pos = strpos($accept, ';')) {
$accept = trim(substr($accept, 0, $pos));
}
if ($accept === '*/*' || $accept === '*') {
return true;
}
foreach ($types as $type) {
$accept = strtolower($accept);
$type = strtolower($type);
if ($this->matchesType($accept, $type) || $accept === strtok($type, '/').'/*') {
return true;
}
}
}
return false;
}
/**
* Return the most suitable content type from the given array based on content negotiation.
*
* @param string|array $contentTypes
* @return string|null
*/
public function prefers($contentTypes)
{
$accepts = $this->getAcceptableContentTypes();
$contentTypes = (array) $contentTypes;
foreach ($accepts as $accept) {
if ($accept && $pos = strpos($accept, ';')) {
$accept = trim(substr($accept, 0, $pos));
}
if (in_array($accept, ['*/*', '*'])) {
return $contentTypes[0];
}
foreach ($contentTypes as $contentType) {
$type = $contentType;
if (! is_null($mimeType = $this->getMimeType($contentType))) {
$type = $mimeType;
}
$accept = strtolower($accept);
$type = strtolower($type);
if ($this->matchesType($type, $accept) || $accept === strtok($type, '/').'/*') {
return $contentType;
}
}
}
}
/**
* Determine if the current request accepts any content type.
*
* @return bool
*/
public function acceptsAnyContentType()
{
$acceptable = $this->getAcceptableContentTypes();
return count($acceptable) === 0 || (
isset($acceptable[0]) && ($acceptable[0] === '*/*' || $acceptable[0] === '*')
);
}
/**
* Determines whether a request accepts JSON.
*
* @return bool
*/
public function acceptsJson()
{
return $this->accepts('application/json');
}
/**
* Determines whether a request accepts HTML.
*
* @return bool
*/
public function acceptsHtml()
{
return $this->accepts('text/html');
}
/**
* Determine if the given content types match.
*
* @param string $actual
* @param string $type
* @return bool
*/
public static function matchesType($actual, $type)
{
if ($actual === $type) {
return true;
}
$split = explode('/', $actual);
return isset($split[1]) && preg_match('#'.preg_quote($split[0], '#').'/.+\+'.preg_quote($split[1], '#').'#', $type);
}
/**
* Get the data format expected in the response.
*
* @param string $default
* @return string
*/
public function format($default = 'html')
{
foreach ($this->getAcceptableContentTypes() as $type) {
if ($format = $this->getFormat($type)) {
return $format;
}
}
return $default;
}
}

View File

@@ -0,0 +1,68 @@
<?php
namespace Illuminate\Http\Concerns;
use Illuminate\Database\Eloquent\Model;
trait InteractsWithFlashData
{
/**
* Retrieve an old input item.
*
* @param string|null $key
* @param \Illuminate\Database\Eloquent\Model|string|array|null $default
* @return string|array|null
*/
public function old($key = null, $default = null)
{
$default = $default instanceof Model ? $default->getAttribute($key) : $default;
return $this->hasSession() ? $this->session()->getOldInput($key, $default) : $default;
}
/**
* Flash the input for the current request to the session.
*
* @return void
*/
public function flash()
{
$this->session()->flashInput($this->input());
}
/**
* Flash only some of the input to the session.
*
* @param mixed $keys
* @return void
*/
public function flashOnly($keys)
{
$this->session()->flashInput(
$this->only(is_array($keys) ? $keys : func_get_args())
);
}
/**
* Flash only some of the input to the session.
*
* @param mixed $keys
* @return void
*/
public function flashExcept($keys)
{
$this->session()->flashInput(
$this->except(is_array($keys) ? $keys : func_get_args())
);
}
/**
* Flush all of the old input from the session.
*
* @return void
*/
public function flush()
{
$this->session()->flashInput([]);
}
}

View File

@@ -0,0 +1,300 @@
<?php
namespace Illuminate\Http\Concerns;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Arr;
use Illuminate\Support\Fluent;
use Illuminate\Support\Traits\Dumpable;
use Illuminate\Support\Traits\InteractsWithData;
use SplFileInfo;
use Symfony\Component\HttpFoundation\InputBag;
trait InteractsWithInput
{
use Dumpable, InteractsWithData;
/**
* Retrieve a server variable from the request.
*
* @param string|null $key
* @param string|array|null $default
* @return string|array|null
*/
public function server($key = null, $default = null)
{
return $this->retrieveItem('server', $key, $default);
}
/**
* Determine if a header is set on the request.
*
* @param string $key
* @return bool
*/
public function hasHeader($key)
{
return ! is_null($this->header($key));
}
/**
* Retrieve a header from the request.
*
* @param string|null $key
* @param string|array|null $default
* @return string|array|null
*/
public function header($key = null, $default = null)
{
return $this->retrieveItem('headers', $key, $default);
}
/**
* Get the bearer token from the request headers.
*
* @return string|null
*/
public function bearerToken()
{
$header = $this->header('Authorization', '');
$position = strripos($header, 'Bearer ');
if ($position !== false) {
$header = substr($header, $position + 7);
return str_contains($header, ',') ? strstr($header, ',', true) : $header;
}
}
/**
* Get the keys for all of the input and files.
*
* @return array
*/
public function keys()
{
return array_merge(array_keys($this->input()), $this->files->keys());
}
/**
* Get all of the input and files for the request.
*
* @param mixed $keys
* @return array
*/
public function all($keys = null)
{
$input = array_replace_recursive($this->input(), $this->allFiles());
if (! $keys) {
return $input;
}
$results = [];
foreach (is_array($keys) ? $keys : func_get_args() as $key) {
Arr::set($results, $key, Arr::get($input, $key));
}
return $results;
}
/**
* Retrieve an input item from the request.
*
* @param string|null $key
* @param mixed $default
* @return mixed
*/
public function input($key = null, $default = null)
{
return data_get(
$this->getInputSource()->all() + $this->query->all(), $key, $default
);
}
/**
* Retrieve input from the request as a Fluent object instance.
*
* @param array|string|null $key
* @param array $default
* @return \Illuminate\Support\Fluent
*/
public function fluent($key = null, array $default = [])
{
$value = is_array($key) ? $this->only($key) : $this->input($key);
return new Fluent($value ?? $default);
}
/**
* Retrieve a query string item from the request.
*
* @param string|null $key
* @param string|array|null $default
* @return string|array|null
*/
public function query($key = null, $default = null)
{
return $this->retrieveItem('query', $key, $default);
}
/**
* Retrieve a request payload item from the request.
*
* @param string|null $key
* @param string|array|null $default
* @return string|array|null
*/
public function post($key = null, $default = null)
{
return $this->retrieveItem('request', $key, $default);
}
/**
* Determine if a cookie is set on the request.
*
* @param string $key
* @return bool
*/
public function hasCookie($key)
{
return ! is_null($this->cookie($key));
}
/**
* Retrieve a cookie from the request.
*
* @param string|null $key
* @param string|array|null $default
* @return string|array|null
*/
public function cookie($key = null, $default = null)
{
return $this->retrieveItem('cookies', $key, $default);
}
/**
* Get an array of all of the files on the request.
*
* @return array<string, \Illuminate\Http\UploadedFile|\Illuminate\Http\UploadedFile[]>
*/
public function allFiles()
{
$files = $this->files->all();
return $this->convertedFiles = $this->convertedFiles ?? $this->convertUploadedFiles($files);
}
/**
* Convert the given array of Symfony UploadedFiles to custom Laravel UploadedFiles.
*
* @param array<string, \Symfony\Component\HttpFoundation\File\UploadedFile|\Symfony\Component\HttpFoundation\File\UploadedFile[]> $files
* @return array<string, \Illuminate\Http\UploadedFile|\Illuminate\Http\UploadedFile[]>
*/
protected function convertUploadedFiles(array $files)
{
return array_map(function ($file) {
if (is_null($file) || (is_array($file) && empty(array_filter($file)))) {
return $file;
}
return is_array($file)
? $this->convertUploadedFiles($file)
: UploadedFile::createFromBase($file);
}, $files);
}
/**
* Determine if the uploaded data contains a file.
*
* @param string $key
* @return bool
*/
public function hasFile($key)
{
if (! is_array($files = $this->file($key))) {
$files = [$files];
}
foreach ($files as $file) {
if ($this->isValidFile($file)) {
return true;
}
}
return false;
}
/**
* Check that the given file is a valid file instance.
*
* @param mixed $file
* @return bool
*/
protected function isValidFile($file)
{
return $file instanceof SplFileInfo && $file->getPath() !== '';
}
/**
* Retrieve a file from the request.
*
* @param string|null $key
* @param mixed $default
* @return ($key is null ? array<string, \Illuminate\Http\UploadedFile|\Illuminate\Http\UploadedFile[]> : \Illuminate\Http\UploadedFile|\Illuminate\Http\UploadedFile[]|null)
*/
public function file($key = null, $default = null)
{
return data_get($this->allFiles(), $key, $default);
}
/**
* Retrieve data from the instance.
*
* @param string|null $key
* @param mixed $default
* @return mixed
*/
protected function data($key = null, $default = null)
{
return $this->input($key, $default);
}
/**
* Retrieve a parameter item from a given source.
*
* @param string $source
* @param string|null $key
* @param string|array|null $default
* @return string|array|null
*/
protected function retrieveItem($source, $key, $default)
{
if (is_null($key)) {
return $this->$source->all();
}
if ($this->$source instanceof InputBag) {
return $this->$source->all()[$key] ?? $default;
}
return $this->$source->get($key, $default);
}
/**
* Dump the items.
*
* @param mixed $keys
* @return $this
*/
public function dump($keys = [])
{
$keys = is_array($keys) ? $keys : func_get_args();
dump(count($keys) > 0 ? $this->only($keys) : $this->all());
return $this;
}
}

View File

@@ -0,0 +1,40 @@
<?php
namespace Illuminate\Http\Exceptions;
use RuntimeException;
use Symfony\Component\HttpFoundation\Response;
use Throwable;
class HttpResponseException extends RuntimeException
{
/**
* The underlying response instance.
*
* @var \Symfony\Component\HttpFoundation\Response
*/
protected $response;
/**
* Create a new HTTP response exception instance.
*
* @param \Symfony\Component\HttpFoundation\Response $response
* @param \Throwable|null $previous
*/
public function __construct(Response $response, ?Throwable $previous = null)
{
parent::__construct($previous?->getMessage() ?? '', $previous?->getCode() ?? 0, $previous);
$this->response = $response;
}
/**
* Get the underlying response instance.
*
* @return \Symfony\Component\HttpFoundation\Response
*/
public function getResponse()
{
return $this->response;
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace Illuminate\Http\Exceptions;
use Symfony\Component\HttpKernel\Exception\HttpException;
class MalformedUrlException extends HttpException
{
/**
* Create a new exception instance.
*/
public function __construct()
{
parent::__construct(400, 'Malformed URL.');
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace Illuminate\Http\Exceptions;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Throwable;
class PostTooLargeException extends HttpException
{
/**
* Create a new "post too large" exception instance.
*
* @param string $message
* @param \Throwable|null $previous
* @param array $headers
* @param int $code
*/
public function __construct($message = '', ?Throwable $previous = null, array $headers = [], $code = 0)
{
parent::__construct(413, $message, $previous, $headers, $code);
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace Illuminate\Http\Exceptions;
use Symfony\Component\HttpKernel\Exception\TooManyRequestsHttpException;
use Throwable;
class ThrottleRequestsException extends TooManyRequestsHttpException
{
/**
* Create a new throttle requests exception instance.
*
* @param string $message
* @param \Throwable|null $previous
* @param array $headers
* @param int $code
*/
public function __construct($message = '', ?Throwable $previous = null, array $headers = [], $code = 0)
{
parent::__construct(null, $message, $previous, $code, $headers);
}
}

View File

@@ -0,0 +1,10 @@
<?php
namespace Illuminate\Http;
use Symfony\Component\HttpFoundation\File\File as SymfonyFile;
class File extends SymfonyFile
{
use FileHelpers;
}

View File

@@ -0,0 +1,66 @@
<?php
namespace Illuminate\Http;
use Illuminate\Support\Str;
trait FileHelpers
{
/**
* The cache copy of the file's hash name.
*
* @var string|null
*/
protected $hashName = null;
/**
* Get the fully qualified path to the file.
*
* @return string
*/
public function path()
{
return $this->getRealPath();
}
/**
* Get the file's extension.
*
* @return string
*/
public function extension()
{
return $this->guessExtension();
}
/**
* Get a filename for the file.
*
* @param string|null $path
* @return string
*/
public function hashName($path = null)
{
if ($path) {
$path = rtrim($path, '/').'/';
}
$hash = $this->hashName ?: $this->hashName = Str::random(40);
if ($extension = $this->guessExtension()) {
$extension = '.'.$extension;
}
return $path.$hash.$extension;
}
/**
* Get the dimensions of the image (if applicable).
*
* @return array|null
*/
public function dimensions()
{
return @getimagesize($this->getRealPath());
}
}

View File

@@ -0,0 +1,140 @@
<?php
namespace Illuminate\Http;
use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Contracts\Support\Jsonable;
use Illuminate\Support\Traits\Macroable;
use InvalidArgumentException;
use JsonSerializable;
use Symfony\Component\HttpFoundation\JsonResponse as BaseJsonResponse;
class JsonResponse extends BaseJsonResponse
{
use ResponseTrait, Macroable {
Macroable::__call as macroCall;
}
/**
* Create a new JSON response instance.
*
* @param mixed $data
* @param int $status
* @param array $headers
* @param int $options
* @param bool $json
*/
public function __construct($data = null, $status = 200, $headers = [], $options = 0, $json = false)
{
$this->encodingOptions = $options;
parent::__construct($data, $status, $headers, $json);
}
/**
* {@inheritdoc}
*
* @return static
*/
#[\Override]
public static function fromJsonString(?string $data = null, int $status = 200, array $headers = []): static
{
return new static($data, $status, $headers, 0, true);
}
/**
* Sets the JSONP callback.
*
* @param string|null $callback
* @return $this
*/
public function withCallback($callback = null)
{
return $this->setCallback($callback);
}
/**
* Get the json_decoded data from the response.
*
* @param bool $assoc
* @param int $depth
* @return mixed
*/
public function getData($assoc = false, $depth = 512)
{
return json_decode($this->data, $assoc, $depth);
}
/**
* {@inheritdoc}
*
* @return static
*
* @throws \InvalidArgumentException
*/
#[\Override]
public function setData($data = []): static
{
$this->original = $data;
// Ensure json_last_error() is cleared...
json_decode('[]');
$this->data = match (true) {
$data instanceof Jsonable => $data->toJson($this->encodingOptions),
$data instanceof JsonSerializable => json_encode($data->jsonSerialize(), $this->encodingOptions),
$data instanceof Arrayable => json_encode($data->toArray(), $this->encodingOptions),
default => json_encode($data, $this->encodingOptions),
};
if (! $this->hasValidJson(json_last_error())) {
throw new InvalidArgumentException(json_last_error_msg());
}
return $this->update();
}
/**
* Determine if an error occurred during JSON encoding.
*
* @param int $jsonError
* @return bool
*/
protected function hasValidJson($jsonError)
{
if ($jsonError === JSON_ERROR_NONE) {
return true;
}
return $this->hasEncodingOption(JSON_PARTIAL_OUTPUT_ON_ERROR) &&
in_array($jsonError, [
JSON_ERROR_RECURSION,
JSON_ERROR_INF_OR_NAN,
JSON_ERROR_UNSUPPORTED_TYPE,
]);
}
/**
* {@inheritdoc}
*
* @return static
*/
#[\Override]
public function setEncodingOptions($options): static
{
$this->encodingOptions = (int) $options;
return $this->setData($this->getData());
}
/**
* Determine if a JSON encoding option is set.
*
* @param int $option
* @return bool
*/
public function hasEncodingOption($option)
{
return (bool) ($this->encodingOptions & $option);
}
}

View File

@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) Taylor Otwell
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@@ -0,0 +1,41 @@
<?php
namespace Illuminate\Http\Middleware;
use Illuminate\Http\Response;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Vite;
class AddLinkHeadersForPreloadedAssets
{
/**
* Configure the middleware.
*
* @param int $limit
* @return string
*/
public static function using($limit)
{
return static::class.':'.$limit;
}
/**
* Handle the incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @param int|null $limit
* @return \Illuminate\Http\Response
*/
public function handle($request, $next, $limit = null)
{
return tap($next($request), function ($response) use ($limit) {
if ($response instanceof Response && Vite::preloadedAssets() !== []) {
$response->header('Link', (new Collection(Vite::preloadedAssets()))
->when($limit, fn ($assets, $limit) => $assets->take($limit))
->map(fn ($attributes, $url) => "<{$url}>; ".implode('; ', $attributes))
->join(', '), false);
}
});
}
}

View File

@@ -0,0 +1,27 @@
<?php
namespace Illuminate\Http\Middleware;
use Closure;
use Symfony\Component\HttpFoundation\Response;
class CheckResponseForModifications
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
{
$response = $next($request);
if ($response instanceof Response) {
$response->isNotModified($request);
}
return $response;
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace Illuminate\Http\Middleware;
use Closure;
class FrameGuard
{
/**
* Handle the given request and get the response.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return \Symfony\Component\HttpFoundation\Response
*/
public function handle($request, Closure $next)
{
$response = $next($request);
$response->headers->set('X-Frame-Options', 'SAMEORIGIN', false);
return $response;
}
}

View File

@@ -0,0 +1,145 @@
<?php
namespace Illuminate\Http\Middleware;
use Closure;
use Fruitcake\Cors\CorsService;
use Illuminate\Contracts\Container\Container;
use Illuminate\Http\Request;
class HandleCors
{
/**
* The container instance.
*
* @var \Illuminate\Contracts\Container\Container
*/
protected $container;
/**
* The CORS service instance.
*
* @var \Fruitcake\Cors\CorsService
*/
protected $cors;
/**
* All of the registered skip callbacks.
*
* @var array<int, \Closure(\Illuminate\Http\Request): bool>
*/
protected static $skipCallbacks = [];
/**
* Create a new middleware instance.
*
* @param \Illuminate\Contracts\Container\Container $container
* @param \Fruitcake\Cors\CorsService $cors
*/
public function __construct(Container $container, CorsService $cors)
{
$this->container = $container;
$this->cors = $cors;
}
/**
* Handle the incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return \Illuminate\Http\Response
*/
public function handle($request, Closure $next)
{
foreach (static::$skipCallbacks as $callback) {
if ($callback($request)) {
return $next($request);
}
}
if (! $this->hasMatchingPath($request)) {
return $next($request);
}
$this->cors->setOptions($this->container['config']->get('cors', []));
if ($this->cors->isPreflightRequest($request)) {
$response = $this->cors->handlePreflightRequest($request);
$this->cors->varyHeader($response, 'Access-Control-Request-Method');
return $response;
}
$response = $next($request);
if ($request->getMethod() === 'OPTIONS') {
$this->cors->varyHeader($response, 'Access-Control-Request-Method');
}
return $this->cors->addActualRequestHeaders($response, $request);
}
/**
* Get the path from the configuration to determine if the CORS service should run.
*
* @param \Illuminate\Http\Request $request
* @return bool
*/
protected function hasMatchingPath(Request $request): bool
{
$paths = $this->getPathsByHost($request->getHost());
foreach ($paths as $path) {
if ($path !== '/') {
$path = trim($path, '/');
}
if ($request->fullUrlIs($path) || $request->is($path)) {
return true;
}
}
return false;
}
/**
* Get the CORS paths for the given host.
*
* @param string $host
* @return array
*/
protected function getPathsByHost(string $host)
{
$paths = $this->container['config']->get('cors.paths', []);
if (isset($paths[$host])) {
return $paths[$host];
}
return array_filter($paths, function ($path) {
return is_string($path);
});
}
/**
* Register a callback that instructs the middleware to be skipped.
*
* @param \Closure $callback
* @return void
*/
public static function skipWhen(Closure $callback)
{
static::$skipCallbacks[] = $callback;
}
/**
* Flush the middleware's global state.
*
* @return void
*/
public static function flushState()
{
static::$skipCallbacks = [];
}
}

View File

@@ -0,0 +1,97 @@
<?php
namespace Illuminate\Http\Middleware;
use Closure;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Symfony\Component\HttpFoundation\StreamedResponse;
class SetCacheHeaders
{
/**
* Specify the options for the middleware.
*
* @param array|string $options
* @return string
*/
public static function using($options)
{
if (is_string($options)) {
return static::class.':'.$options;
}
return (new Collection($options))
->map(function ($value, $key) {
if (is_bool($value)) {
return $value ? $key : null;
}
return is_int($key) ? $value : "{$key}={$value}";
})
->filter()
->map(fn ($value) => Str::finish($value, ';'))
->pipe(fn ($options) => rtrim(static::class.':'.$options->implode(''), ';'));
}
/**
* Add cache related HTTP headers.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @param string|array $options
* @return \Symfony\Component\HttpFoundation\Response
*
* @throws \InvalidArgumentException
*/
public function handle($request, Closure $next, $options = [])
{
$response = $next($request);
if (! $request->isMethodCacheable() || (! $response->getContent() && ! $response instanceof BinaryFileResponse && ! $response instanceof StreamedResponse)) {
return $response;
}
if (is_string($options)) {
$options = $this->parseOptions($options);
}
if (! $response->isSuccessful()) {
return $response;
}
if (isset($options['etag']) && $options['etag'] === true) {
$options['etag'] = $response->getEtag() ?? ($response->getContent() ? hash('xxh128', $response->getContent()) : null);
}
if (isset($options['last_modified'])) {
if (is_numeric($options['last_modified'])) {
$options['last_modified'] = Carbon::createFromTimestamp($options['last_modified'], date_default_timezone_get());
} else {
$options['last_modified'] = Carbon::parse($options['last_modified']);
}
}
$response->setCache($options);
$response->isNotModified($request);
return $response;
}
/**
* Parse the given header options.
*
* @param string $options
* @return array
*/
protected function parseOptions($options)
{
return (new Collection(explode(';', rtrim($options, ';'))))->mapWithKeys(function ($option) {
$data = explode('=', $option, 2);
return [$data[0] => $data[1] ?? true];
})->all();
}
}

View File

@@ -0,0 +1,127 @@
<?php
namespace Illuminate\Http\Middleware;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Http\Request;
class TrustHosts
{
/**
* The application instance.
*
* @var \Illuminate\Contracts\Foundation\Application
*/
protected $app;
/**
* The trusted hosts that have been configured to always be trusted.
*
* @var array<int, string>|(callable(): array<int, string>)|null
*/
protected static $alwaysTrust;
/**
* Indicates whether subdomains of the application URL should be trusted.
*
* @var bool|null
*/
protected static $subdomains;
/**
* Create a new middleware instance.
*
* @param \Illuminate\Contracts\Foundation\Application $app
*/
public function __construct(Application $app)
{
$this->app = $app;
}
/**
* Get the host patterns that should be trusted.
*
* @return array
*/
public function hosts()
{
if (is_null(static::$alwaysTrust)) {
return [$this->allSubdomainsOfApplicationUrl()];
}
$hosts = match (true) {
is_array(static::$alwaysTrust) => static::$alwaysTrust,
is_callable(static::$alwaysTrust) => call_user_func(static::$alwaysTrust),
default => [],
};
if (static::$subdomains) {
$hosts[] = $this->allSubdomainsOfApplicationUrl();
}
return $hosts;
}
/**
* Handle the incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return \Illuminate\Http\Response
*/
public function handle(Request $request, $next)
{
if ($this->shouldSpecifyTrustedHosts()) {
Request::setTrustedHosts(array_filter($this->hosts()));
}
return $next($request);
}
/**
* Specify the hosts that should always be trusted.
*
* @param array<int, string>|(callable(): array<int, string>) $hosts
* @param bool $subdomains
* @return void
*/
public static function at(array|callable $hosts, bool $subdomains = true)
{
static::$alwaysTrust = $hosts;
static::$subdomains = $subdomains;
}
/**
* Determine if the application should specify trusted hosts.
*
* @return bool
*/
protected function shouldSpecifyTrustedHosts()
{
return ! $this->app->environment('local') &&
! $this->app->runningUnitTests();
}
/**
* Get a regular expression matching the application URL and all of its subdomains.
*
* @return string|null
*/
protected function allSubdomainsOfApplicationUrl()
{
if ($host = parse_url($this->app['config']->get('app.url'), PHP_URL_HOST)) {
return '^(.+\.)?'.preg_quote($host).'$';
}
}
/**
* Flush the state of the middleware.
*
* @return void
*/
public static function flushState()
{
static::$alwaysTrust = null;
static::$subdomains = null;
}
}

View File

@@ -0,0 +1,202 @@
<?php
namespace Illuminate\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
class TrustProxies
{
/**
* The trusted proxies for the application.
*
* @var array<int, string>|string|null
*/
protected $proxies;
/**
* The trusted proxies headers for the application.
*
* @var int
*/
protected $headers = Request::HEADER_X_FORWARDED_FOR |
Request::HEADER_X_FORWARDED_HOST |
Request::HEADER_X_FORWARDED_PORT |
Request::HEADER_X_FORWARDED_PROTO |
Request::HEADER_X_FORWARDED_PREFIX |
Request::HEADER_X_FORWARDED_AWS_ELB;
/**
* The proxies that have been configured to always be trusted.
*
* @var array<int, string>|string|null
*/
protected static $alwaysTrustProxies;
/**
* The proxies headers that have been configured to always be trusted.
*
* @var int|null
*/
protected static $alwaysTrustHeaders;
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*
* @throws \Symfony\Component\HttpKernel\Exception\HttpException
*/
public function handle(Request $request, Closure $next)
{
$request::setTrustedProxies([], $this->getTrustedHeaderNames());
$this->setTrustedProxyIpAddresses($request);
return $next($request);
}
/**
* Sets the trusted proxies on the request.
*
* @param \Illuminate\Http\Request $request
* @return void
*/
protected function setTrustedProxyIpAddresses(Request $request)
{
$trustedIps = $this->proxies() ?: config('trustedproxy.proxies');
if (is_null($trustedIps) &&
(laravel_cloud() ||
str_ends_with($request->host(), '.on-forge.com') ||
str_ends_with($request->host(), '.on-vapor.com'))) {
$trustedIps = '*';
}
if (str_ends_with($request->host(), '.on-forge.com') ||
str_ends_with($request->host(), '.on-vapor.com')) {
$request->headers->remove('X-Forwarded-Host');
}
if ($trustedIps === '*' || $trustedIps === '**') {
return $this->setTrustedProxyIpAddressesToTheCallingIp($request);
}
$trustedIps = is_string($trustedIps)
? array_map(trim(...), explode(',', $trustedIps))
: $trustedIps;
if (is_array($trustedIps)) {
return $this->setTrustedProxyIpAddressesToSpecificIps($request, $trustedIps);
}
}
/**
* Specify the IP addresses to trust explicitly.
*
* @param \Illuminate\Http\Request $request
* @param array $trustedIps
* @return void
*/
protected function setTrustedProxyIpAddressesToSpecificIps(Request $request, array $trustedIps)
{
$request->setTrustedProxies(array_reduce($trustedIps, function ($ips, $trustedIp) use ($request) {
$ips[] = $trustedIp === 'REMOTE_ADDR'
? $request->server->get('REMOTE_ADDR')
: $trustedIp;
return $ips;
}, []), $this->getTrustedHeaderNames());
}
/**
* Set the trusted proxy to be the IP address calling this servers.
*
* @param \Illuminate\Http\Request $request
* @return void
*/
protected function setTrustedProxyIpAddressesToTheCallingIp(Request $request)
{
$request->setTrustedProxies([$request->server->get('REMOTE_ADDR')], $this->getTrustedHeaderNames());
}
/**
* Retrieve trusted header name(s), falling back to defaults if config not set.
*
* @return int A bit field of Request::HEADER_*, to set which headers to trust from your proxies.
*/
protected function getTrustedHeaderNames()
{
$headers = $this->headers();
if (is_int($headers)) {
return $headers;
}
return match ($headers) {
'HEADER_X_FORWARDED_AWS_ELB' => Request::HEADER_X_FORWARDED_AWS_ELB,
'HEADER_FORWARDED' => Request::HEADER_FORWARDED,
'HEADER_X_FORWARDED_FOR' => Request::HEADER_X_FORWARDED_FOR,
'HEADER_X_FORWARDED_HOST' => Request::HEADER_X_FORWARDED_HOST,
'HEADER_X_FORWARDED_PORT' => Request::HEADER_X_FORWARDED_PORT,
'HEADER_X_FORWARDED_PROTO' => Request::HEADER_X_FORWARDED_PROTO,
'HEADER_X_FORWARDED_PREFIX' => Request::HEADER_X_FORWARDED_PREFIX,
default => Request::HEADER_X_FORWARDED_FOR | Request::HEADER_X_FORWARDED_HOST | Request::HEADER_X_FORWARDED_PORT | Request::HEADER_X_FORWARDED_PROTO | Request::HEADER_X_FORWARDED_PREFIX | Request::HEADER_X_FORWARDED_AWS_ELB,
};
}
/**
* Get the trusted headers.
*
* @return int
*/
protected function headers()
{
return static::$alwaysTrustHeaders ?: $this->headers;
}
/**
* Get the trusted proxies.
*
* @return array|string|null
*/
protected function proxies()
{
return static::$alwaysTrustProxies ?: $this->proxies;
}
/**
* Specify the IP addresses of proxies that should always be trusted.
*
* @param array|string $proxies
* @return void
*/
public static function at(array|string $proxies)
{
static::$alwaysTrustProxies = $proxies;
}
/**
* Specify the proxy headers that should always be trusted.
*
* @param int $headers
* @return void
*/
public static function withHeaders(int $headers)
{
static::$alwaysTrustHeaders = $headers;
}
/**
* Flush the state of the middleware.
*
* @return void
*/
public static function flushState()
{
static::$alwaysTrustHeaders = null;
static::$alwaysTrustProxies = null;
}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace Illuminate\Http\Middleware;
use Closure;
use Illuminate\Http\Exceptions\MalformedUrlException;
use Illuminate\Http\Request;
class ValidatePathEncoding
{
/**
* Validate that the incoming request has a valid UTF-8 encoded path.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return \Symfony\Component\HttpFoundation\Response
*/
public function handle(Request $request, Closure $next)
{
$decodedPath = rawurldecode($request->path());
if (! mb_check_encoding($decodedPath, 'UTF-8')) {
throw new MalformedUrlException;
}
return $next($request);
}
}

View File

@@ -0,0 +1,52 @@
<?php
namespace Illuminate\Http\Middleware;
use Closure;
use Illuminate\Http\Exceptions\PostTooLargeException;
class ValidatePostSize
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*
* @throws \Illuminate\Http\Exceptions\PostTooLargeException
*/
public function handle($request, Closure $next)
{
$max = $this->getPostMaxSize();
if ($max > 0 && $request->server('CONTENT_LENGTH') > $max) {
throw new PostTooLargeException('The POST data is too large.');
}
return $next($request);
}
/**
* Determine the server 'post_max_size' as bytes.
*
* @return int
*/
protected function getPostMaxSize()
{
if (is_numeric($postMaxSize = ini_get('post_max_size'))) {
return (int) $postMaxSize;
}
$metric = strtoupper(substr($postMaxSize, -1));
$postMaxSize = (int) $postMaxSize;
return match ($metric) {
'K' => $postMaxSize * 1024,
'M' => $postMaxSize * 1048576,
'G' => $postMaxSize * 1073741824,
default => $postMaxSize,
};
}
}

View File

@@ -0,0 +1,283 @@
<?php
namespace Illuminate\Http;
use Illuminate\Contracts\Support\MessageProvider;
use Illuminate\Session\Store as SessionStore;
use Illuminate\Support\MessageBag;
use Illuminate\Support\Str;
use Illuminate\Support\Traits\ForwardsCalls;
use Illuminate\Support\Traits\Macroable;
use Illuminate\Support\Uri;
use Illuminate\Support\ViewErrorBag;
use Symfony\Component\HttpFoundation\File\UploadedFile as SymfonyUploadedFile;
use Symfony\Component\HttpFoundation\RedirectResponse as BaseRedirectResponse;
class RedirectResponse extends BaseRedirectResponse
{
use ForwardsCalls, ResponseTrait, Macroable {
Macroable::__call as macroCall;
}
/**
* The request instance.
*
* @var \Illuminate\Http\Request
*/
protected $request;
/**
* The session store instance.
*
* @var \Illuminate\Session\Store
*/
protected $session;
/**
* Flash a piece of data to the session.
*
* @param string|array $key
* @param mixed $value
* @return $this
*/
public function with($key, $value = null)
{
$key = is_array($key) ? $key : [$key => $value];
foreach ($key as $k => $v) {
$this->session->flash($k, $v);
}
return $this;
}
/**
* Add multiple cookies to the response.
*
* @param array $cookies
* @return $this
*/
public function withCookies(array $cookies)
{
foreach ($cookies as $cookie) {
$this->headers->setCookie($cookie);
}
return $this;
}
/**
* Flash an array of input to the session.
*
* @param array|null $input
* @return $this
*/
public function withInput(?array $input = null)
{
$this->session->flashInput($this->removeFilesFromInput(
! is_null($input) ? $input : $this->request->input()
));
return $this;
}
/**
* Remove all uploaded files form the given input array.
*
* @param array $input
* @return array
*/
protected function removeFilesFromInput(array $input)
{
foreach ($input as $key => $value) {
if (is_array($value)) {
$input[$key] = $this->removeFilesFromInput($value);
}
if ($value instanceof SymfonyUploadedFile) {
unset($input[$key]);
}
}
return $input;
}
/**
* Flash an array of input to the session.
*
* @return $this
*/
public function onlyInput()
{
return $this->withInput($this->request->only(func_get_args()));
}
/**
* Flash an array of input to the session.
*
* @return $this
*/
public function exceptInput()
{
return $this->withInput($this->request->except(func_get_args()));
}
/**
* Flash a container of errors to the session.
*
* @param \Illuminate\Contracts\Support\MessageProvider|array|string $provider
* @param string $key
* @return $this
*/
public function withErrors($provider, $key = 'default')
{
$value = $this->parseErrors($provider);
$errors = $this->session->get('errors', new ViewErrorBag);
if (! $errors instanceof ViewErrorBag) {
$errors = new ViewErrorBag;
}
$this->session->flash(
'errors', $errors->put($key, $value)
);
return $this;
}
/**
* Parse the given errors into an appropriate value.
*
* @param \Illuminate\Contracts\Support\MessageProvider|array|string $provider
* @return \Illuminate\Support\MessageBag
*/
protected function parseErrors($provider)
{
if ($provider instanceof MessageProvider) {
return $provider->getMessageBag();
}
return new MessageBag((array) $provider);
}
/**
* Add a fragment identifier to the URL.
*
* @param string $fragment
* @return $this
*/
public function withFragment($fragment)
{
return $this->withoutFragment()
->setTargetUrl($this->getTargetUrl().'#'.Str::after($fragment, '#'));
}
/**
* Remove any fragment identifier from the response URL.
*
* @return $this
*/
public function withoutFragment()
{
return $this->setTargetUrl(Str::before($this->getTargetUrl(), '#'));
}
/**
* Enforce that the redirect target must have the same host as the current request.
*/
public function enforceSameOrigin(
string $fallback,
bool $validateScheme = true,
bool $validatePort = true,
): static {
$target = Uri::of($this->targetUrl);
$current = Uri::of($this->request->getSchemeAndHttpHost());
if ($target->host() !== $current->host() ||
($validateScheme && $target->scheme() !== $current->scheme()) ||
($validatePort && $target->port() !== $current->port())) {
$this->setTargetUrl($fallback);
}
return $this;
}
/**
* Get the original response content.
*
* @return null
*/
public function getOriginalContent()
{
//
}
/**
* Get the request instance.
*
* @return \Illuminate\Http\Request|null
*/
public function getRequest()
{
return $this->request;
}
/**
* Set the request instance.
*
* @param \Illuminate\Http\Request $request
* @return $this
*/
public function setRequest(Request $request)
{
$this->request = $request;
return $this;
}
/**
* Get the session store instance.
*
* @return \Illuminate\Session\Store|null
*/
public function getSession()
{
return $this->session;
}
/**
* Set the session store instance.
*
* @param \Illuminate\Session\Store $session
* @return $this
*/
public function setSession(SessionStore $session)
{
$this->session = $session;
return $this;
}
/**
* Dynamically bind flash data in the session.
*
* @param string $method
* @param array $parameters
* @return mixed
*
* @throws \BadMethodCallException
*/
public function __call($method, $parameters)
{
if (static::hasMacro($method)) {
return $this->macroCall($method, $parameters);
}
if (str_starts_with($method, 'with')) {
return $this->with(Str::snake(substr($method, 4)), $parameters[0]);
}
static::throwBadMethodCallException($method);
}
}

View File

@@ -0,0 +1,841 @@
<?php
namespace Illuminate\Http;
use ArrayAccess;
use Closure;
use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Session\SymfonySessionDecorator;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
use Illuminate\Support\Traits\Conditionable;
use Illuminate\Support\Traits\Macroable;
use Illuminate\Support\Uri;
use RuntimeException;
use Symfony\Component\HttpFoundation\Exception\SessionNotFoundException;
use Symfony\Component\HttpFoundation\InputBag;
use Symfony\Component\HttpFoundation\Request as SymfonyRequest;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
/**
* @method array validate(array $rules, ...$params)
* @method array validateWithBag(string $errorBag, array $rules, ...$params)
* @method bool hasValidSignature(bool $absolute = true)
* @method bool hasValidRelativeSignature()
* @method bool hasValidSignatureWhileIgnoring($ignoreQuery = [], $absolute = true)
* @method bool hasValidRelativeSignatureWhileIgnoring($ignoreQuery = [])
*/
class Request extends SymfonyRequest implements Arrayable, ArrayAccess
{
use Concerns\CanBePrecognitive,
Concerns\InteractsWithContentTypes,
Concerns\InteractsWithFlashData,
Concerns\InteractsWithInput,
Conditionable,
Macroable;
/**
* The decoded JSON content for the request.
*
* @var \Symfony\Component\HttpFoundation\InputBag|null
*/
protected $json;
/**
* All of the converted files for the request.
*
* @var array<int, \Illuminate\Http\UploadedFile|\Illuminate\Http\UploadedFile[]>
*/
protected $convertedFiles;
/**
* The user resolver callback.
*
* @var \Closure
*/
protected $userResolver;
/**
* The route resolver callback.
*
* @var \Closure
*/
protected $routeResolver;
/**
* The cached "Accept" header value.
*
* @var string|null
*/
protected $cachedAcceptHeader;
/**
* Create a new Illuminate HTTP request from server variables.
*
* @return static
*/
public static function capture()
{
static::enableHttpMethodParameterOverride();
return static::createFromBase(SymfonyRequest::createFromGlobals());
}
/**
* Return the Request instance.
*
* @return $this
*/
public function instance()
{
return $this;
}
/**
* Get the request method.
*
* @return string
*/
public function method()
{
return $this->getMethod();
}
/**
* Get a URI instance for the request.
*
* @return \Illuminate\Support\Uri
*/
public function uri()
{
return Uri::of($this->fullUrl());
}
/**
* Get the root URL for the application.
*
* @return string
*/
public function root()
{
return rtrim($this->getSchemeAndHttpHost().$this->getBaseUrl(), '/');
}
/**
* Get the URL (no query string) for the request.
*
* @return string
*/
public function url()
{
return rtrim(preg_replace('/\?.*/', '', $this->getUri()), '/');
}
/**
* Get the full URL for the request.
*
* @return string
*/
public function fullUrl()
{
$query = $this->getQueryString();
$question = $this->getBaseUrl().$this->getPathInfo() === '/' ? '/?' : '?';
return $query ? $this->url().$question.$query : $this->url();
}
/**
* Get the full URL for the request with the added query string parameters.
*
* @param array $query
* @return string
*/
public function fullUrlWithQuery(array $query)
{
$question = $this->getBaseUrl().$this->getPathInfo() === '/' ? '/?' : '?';
return count($this->query()) > 0
? $this->url().$question.Arr::query(array_merge($this->query(), $query))
: $this->fullUrl().$question.Arr::query($query);
}
/**
* Get the full URL for the request without the given query string parameters.
*
* @param array|string $keys
* @return string
*/
public function fullUrlWithoutQuery($keys)
{
$query = Arr::except($this->query(), $keys);
$question = $this->getBaseUrl().$this->getPathInfo() === '/' ? '/?' : '?';
return count($query) > 0
? $this->url().$question.Arr::query($query)
: $this->url();
}
/**
* Get the current path info for the request.
*
* @return string
*/
public function path()
{
$pattern = trim($this->getPathInfo(), '/');
return $pattern === '' ? '/' : $pattern;
}
/**
* Get the current decoded path info for the request.
*
* @return string
*/
public function decodedPath()
{
return rawurldecode($this->path());
}
/**
* Get a segment from the URI (1 based index).
*
* @param int $index
* @param string|null $default
* @return string|null
*/
public function segment($index, $default = null)
{
return Arr::get($this->segments(), $index - 1, $default);
}
/**
* Get all of the segments for the request path.
*
* @return array
*/
public function segments()
{
$segments = explode('/', $this->decodedPath());
return array_values(array_filter($segments, function ($value) {
return $value !== '';
}));
}
/**
* Determine if the current request URI matches a pattern.
*
* @param mixed ...$patterns
* @return bool
*/
public function is(...$patterns)
{
return (new Collection($patterns))
->contains(fn ($pattern) => Str::is($pattern, $this->decodedPath()));
}
/**
* Determine if the route name matches a given pattern.
*
* @param mixed ...$patterns
* @return bool
*/
public function routeIs(...$patterns)
{
return $this->route() && $this->route()->named(...$patterns);
}
/**
* Determine if the current request URL and query string match a pattern.
*
* @param mixed ...$patterns
* @return bool
*/
public function fullUrlIs(...$patterns)
{
return (new Collection($patterns))
->contains(fn ($pattern) => Str::is($pattern, $this->fullUrl()));
}
/**
* Get the host name.
*
* @return string
*/
public function host()
{
return $this->getHost();
}
/**
* Get the HTTP host being requested.
*
* @return string
*/
public function httpHost()
{
return $this->getHttpHost();
}
/**
* Get the scheme and HTTP host.
*
* @return string
*/
public function schemeAndHttpHost()
{
return $this->getSchemeAndHttpHost();
}
/**
* Determine if the request is the result of an AJAX call.
*
* @return bool
*/
public function ajax()
{
return $this->isXmlHttpRequest();
}
/**
* Determine if the request is the result of a PJAX call.
*
* @return bool
*/
public function pjax()
{
return $this->headers->get('X-PJAX') == true;
}
/**
* Determine if the request is the result of a prefetch call.
*
* @return bool
*/
public function prefetch()
{
return strcasecmp($this->server->get('HTTP_X_MOZ') ?? '', 'prefetch') === 0 ||
strcasecmp($this->headers->get('Purpose') ?? '', 'prefetch') === 0 ||
strcasecmp($this->headers->get('Sec-Purpose') ?? '', 'prefetch') === 0;
}
/**
* Determine if the request is over HTTPS.
*
* @return bool
*/
public function secure()
{
return $this->isSecure();
}
/**
* Get the client IP address.
*
* @return string|null
*/
public function ip()
{
return $this->getClientIp();
}
/**
* Get the client IP addresses.
*
* @return array
*/
public function ips()
{
return $this->getClientIps();
}
/**
* Get the client user agent.
*
* @return string|null
*/
public function userAgent()
{
return $this->headers->get('User-Agent');
}
/**
* {@inheritdoc}
*/
#[\Override]
public function getAcceptableContentTypes(): array
{
$currentAcceptHeader = $this->headers->get('Accept');
if ($this->cachedAcceptHeader !== $currentAcceptHeader) {
// Flush acceptable content types so Symfony re-calculates them...
$this->acceptableContentTypes = null;
$this->cachedAcceptHeader = $currentAcceptHeader;
}
return parent::getAcceptableContentTypes();
}
/**
* Merge new input into the current request's input array.
*
* @param array $input
* @return $this
*/
public function merge(array $input)
{
return tap($this, function (Request $request) use ($input) {
$request->getInputSource()
->replace((new Collection($input))->reduce(
fn ($requestInput, $value, $key) => data_set($requestInput, $key, $value),
$this->getInputSource()->all()
));
});
}
/**
* Merge new input into the request's input, but only when that key is missing from the request.
*
* @param array $input
* @return $this
*/
public function mergeIfMissing(array $input)
{
return $this->merge((new Collection($input))
->filter(fn ($value, $key) => $this->missing($key))
->toArray()
);
}
/**
* Replace the input values for the current request.
*
* @param array $input
* @return $this
*/
public function replace(array $input)
{
$this->getInputSource()->replace($input);
return $this;
}
/**
* This method belongs to Symfony HttpFoundation and is not usually needed when using Laravel.
*
* Instead, you may use the "input" method.
*
* @param string $key
* @param mixed $default
* @return mixed
*
* @deprecated use ->input() instead
*/
#[\Override]
public function get(string $key, mixed $default = null): mixed
{
return parent::get($key, $default);
}
/**
* Get the JSON payload for the request.
*
* @param string|null $key
* @param mixed $default
* @return ($key is null ? \Symfony\Component\HttpFoundation\InputBag : mixed)
*/
public function json($key = null, $default = null)
{
if (! isset($this->json)) {
$this->json = new InputBag((array) json_decode($this->getContent() ?: '[]', true));
}
if (is_null($key)) {
return $this->json;
}
return data_get($this->json->all(), $key, $default);
}
/**
* Get the input source for the request.
*
* @return \Symfony\Component\HttpFoundation\InputBag
*/
protected function getInputSource()
{
if ($this->isJson()) {
return $this->json();
}
return in_array($this->getRealMethod(), ['GET', 'HEAD']) ? $this->query : $this->request;
}
/**
* Create a new request instance from the given Laravel request.
*
* @param \Illuminate\Http\Request $from
* @param \Illuminate\Http\Request|null $to
* @return static
*/
public static function createFrom(self $from, $to = null)
{
$request = $to ?: new static;
$files = array_filter($from->files->all());
$request->initialize(
$from->query->all(),
$from->request->all(),
$from->attributes->all(),
$from->cookies->all(),
$files,
$from->server->all(),
$from->getContent()
);
$request->headers->replace($from->headers->all());
$request->setRequestLocale($from->getLocale());
$request->setDefaultRequestLocale($from->getDefaultLocale());
$request->setJson($from->json());
if ($from->hasSession() && $session = $from->session()) {
$request->setLaravelSession($session);
}
$request->setUserResolver($from->getUserResolver());
$request->setRouteResolver($from->getRouteResolver());
return $request;
}
/**
* Create an Illuminate request from a Symfony instance.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* @return static
*/
public static function createFromBase(SymfonyRequest $request)
{
$newRequest = new static(
$request->query->all(), $request->request->all(), $request->attributes->all(),
$request->cookies->all(), (new static)->filterFiles($request->files->all()) ?? [], $request->server->all()
);
$newRequest->headers->replace($request->headers->all());
$newRequest->content = $request->content;
if ($newRequest->isJson()) {
$newRequest->request = $newRequest->json();
}
return $newRequest;
}
/**
* {@inheritdoc}
*
* @return static
*/
#[\Override]
public function duplicate(?array $query = null, ?array $request = null, ?array $attributes = null, ?array $cookies = null, ?array $files = null, ?array $server = null): static
{
return parent::duplicate($query, $request, $attributes, $cookies, $this->filterFiles($files), $server);
}
/**
* Filter the given array of files, removing any empty values.
*
* @param mixed $files
* @return mixed
*/
protected function filterFiles($files)
{
if (! $files) {
return;
}
foreach ($files as $key => $file) {
if (is_array($file)) {
$files[$key] = $this->filterFiles($files[$key]);
}
if (empty($files[$key])) {
unset($files[$key]);
}
}
return $files;
}
/**
* {@inheritdoc}
*/
#[\Override]
public function hasSession(bool $skipIfUninitialized = false): bool
{
return $this->session instanceof SymfonySessionDecorator;
}
/**
* {@inheritdoc}
*/
#[\Override]
public function getSession(): SessionInterface
{
return $this->hasSession()
? $this->session
: throw new SessionNotFoundException;
}
/**
* Get the session associated with the request.
*
* @return \Illuminate\Contracts\Session\Session
*
* @throws \RuntimeException
*/
public function session()
{
if (! $this->hasSession()) {
throw new RuntimeException('Session store not set on request.');
}
return $this->session->store;
}
/**
* Set the session instance on the request.
*
* @param \Illuminate\Contracts\Session\Session $session
* @return void
*/
public function setLaravelSession($session)
{
$this->session = new SymfonySessionDecorator($session);
}
/**
* Set the locale for the request instance.
*
* @param string $locale
* @return void
*/
public function setRequestLocale(string $locale)
{
$this->locale = $locale;
}
/**
* Set the default locale for the request instance.
*
* @param string $locale
* @return void
*/
public function setDefaultRequestLocale(string $locale)
{
$this->defaultLocale = $locale;
}
/**
* Get the user making the request.
*
* @param string|null $guard
* @return mixed
*/
public function user($guard = null)
{
return call_user_func($this->getUserResolver(), $guard);
}
/**
* Get the route handling the request.
*
* @param string|null $param
* @param mixed $default
* @return ($param is null ? \Illuminate\Routing\Route : object|string|null)
*/
public function route($param = null, $default = null)
{
$route = call_user_func($this->getRouteResolver());
if (is_null($route) || is_null($param)) {
return $route;
}
return $route->parameter($param, $default);
}
/**
* Get a unique fingerprint for the request / route / IP address.
*
* @return string
*
* @throws \RuntimeException
*/
public function fingerprint()
{
if (! $route = $this->route()) {
throw new RuntimeException('Unable to generate fingerprint. Route unavailable.');
}
return sha1(implode('|', array_merge(
$route->methods(),
[$route->getDomain(), $route->uri(), $this->ip()]
)));
}
/**
* Set the JSON payload for the request.
*
* @param \Symfony\Component\HttpFoundation\InputBag $json
* @return $this
*/
public function setJson($json)
{
$this->json = $json;
return $this;
}
/**
* Get the user resolver callback.
*
* @return \Closure
*/
public function getUserResolver()
{
return $this->userResolver ?: function () {
//
};
}
/**
* Set the user resolver callback.
*
* @param \Closure $callback
* @return $this
*/
public function setUserResolver(Closure $callback)
{
$this->userResolver = $callback;
return $this;
}
/**
* Get the route resolver callback.
*
* @return \Closure
*/
public function getRouteResolver()
{
return $this->routeResolver ?: function () {
//
};
}
/**
* Set the route resolver callback.
*
* @param \Closure $callback
* @return $this
*/
public function setRouteResolver(Closure $callback)
{
$this->routeResolver = $callback;
return $this;
}
/**
* Get all of the input and files for the request.
*
* @return array
*/
public function toArray(): array
{
return $this->all();
}
/**
* Determine if the given offset exists.
*
* @param string $offset
* @return bool
*/
public function offsetExists($offset): bool
{
$route = $this->route();
return Arr::has(
$this->all() + ($route ? $route->parameters() : []),
$offset
);
}
/**
* Get the value at the given offset.
*
* @param string $offset
* @return mixed
*/
public function offsetGet($offset): mixed
{
return $this->__get($offset);
}
/**
* Set the value at the given offset.
*
* @param string $offset
* @param mixed $value
* @return void
*/
public function offsetSet($offset, $value): void
{
$this->getInputSource()->set($offset, $value);
}
/**
* Remove the value at the given offset.
*
* @param string $offset
* @return void
*/
public function offsetUnset($offset): void
{
$this->getInputSource()->remove($offset);
}
/**
* Check if an input element is set on the request.
*
* @param string $key
* @return bool
*/
public function __isset($key)
{
return ! is_null($this->__get($key));
}
/**
* Get an input element from the request.
*
* @param string $key
* @return mixed
*/
public function __get($key)
{
return Arr::get($this->all(), $key, fn () => $this->route($key));
}
}

View File

@@ -0,0 +1,96 @@
<?php
namespace Illuminate\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Pagination\AbstractCursorPaginator;
use Illuminate\Pagination\AbstractPaginator;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
use LogicException;
use ReflectionClass;
use Traversable;
trait CollectsResources
{
/**
* Map the given collection resource into its individual resources.
*
* @param mixed $resource
* @return mixed
*/
protected function collectResource($resource)
{
if ($resource instanceof MissingValue) {
return $resource;
}
if (is_array($resource)) {
$resource = new Collection($resource);
}
$collects = $this->collects();
$this->collection = $collects && ! $resource->first() instanceof $collects
? $resource->mapInto($collects)
: $resource->toBase();
return ($resource instanceof AbstractPaginator || $resource instanceof AbstractCursorPaginator)
? $resource->setCollection($this->collection)
: $this->collection;
}
/**
* Get the resource that this resource collects.
*
* @return class-string<\Illuminate\Http\Resources\Json\JsonResource>|null
*/
protected function collects()
{
$collects = null;
if ($this->collects) {
$collects = $this->collects;
} elseif (str_ends_with(class_basename($this), 'Collection') &&
(class_exists($class = Str::replaceLast('Collection', '', get_class($this))) ||
class_exists($class = Str::replaceLast('Collection', 'Resource', get_class($this))))) {
$collects = $class;
}
if (! $collects || is_a($collects, JsonResource::class, true)) {
return $collects;
}
throw new LogicException('Resource collections must collect instances of '.JsonResource::class.'.');
}
/**
* Get the JSON serialization options that should be applied to the resource response.
*
* @return int
*
* @throws \ReflectionException
*/
public function jsonOptions()
{
$collects = $this->collects();
if (! $collects) {
return 0;
}
return (new ReflectionClass($collects))
->newInstanceWithoutConstructor()
->jsonOptions();
}
/**
* Get an iterator for the resource collection.
*
* @return \ArrayIterator
*/
public function getIterator(): Traversable
{
return $this->collection->getIterator();
}
}

View File

@@ -0,0 +1,439 @@
<?php
namespace Illuminate\Http\Resources;
use Illuminate\Support\Arr;
use Illuminate\Support\Stringable;
trait ConditionallyLoadsAttributes
{
/**
* Filter the given data, removing any optional values.
*
* @param array $data
* @return array
*/
protected function filter($data)
{
$index = -1;
foreach ($data as $key => $value) {
$index++;
if (is_array($value)) {
$data[$key] = $this->filter($value);
continue;
}
if (is_numeric($key) && $value instanceof MergeValue) {
return $this->mergeData(
$data, $index, $this->filter($value->data),
array_values($value->data) === $value->data
);
}
if ($value instanceof self && is_null($value->resource)) {
$data[$key] = null;
}
}
return $this->removeMissingValues($data);
}
/**
* Merge the given data in at the given index.
*
* @param array $data
* @param int $index
* @param array $merge
* @param bool $numericKeys
* @return array
*/
protected function mergeData($data, $index, $merge, $numericKeys)
{
if ($numericKeys) {
return $this->removeMissingValues(array_merge(
array_merge(array_slice($data, 0, $index, true), $merge),
$this->filter(array_values(array_slice($data, $index + 1, null, true)))
));
}
return $this->removeMissingValues(array_slice($data, 0, $index, true) +
$merge +
$this->filter(array_slice($data, $index + 1, null, true)));
}
/**
* Remove the missing values from the filtered data.
*
* @param array $data
* @return array
*/
protected function removeMissingValues($data)
{
$numericKeys = true;
foreach ($data as $key => $value) {
if (($value instanceof PotentiallyMissing && $value->isMissing()) ||
($value instanceof self &&
$value->resource instanceof PotentiallyMissing &&
$value->isMissing())) {
unset($data[$key]);
} else {
$numericKeys = $numericKeys && is_numeric($key);
}
}
if (property_exists($this, 'preserveKeys') && $this->preserveKeys === true) {
return $data;
}
return $numericKeys ? array_values($data) : $data;
}
/**
* Retrieve a value if the given "condition" is truthy.
*
* @param bool $condition
* @param mixed $value
* @param mixed $default
* @return \Illuminate\Http\Resources\MissingValue|mixed
*/
protected function when($condition, $value, $default = new MissingValue)
{
if ($condition) {
return value($value);
}
return func_num_args() === 3 ? value($default) : $default;
}
/**
* Retrieve a value if the given "condition" is falsy.
*
* @param bool $condition
* @param mixed $value
* @param mixed $default
* @return \Illuminate\Http\Resources\MissingValue|mixed
*/
public function unless($condition, $value, $default = new MissingValue)
{
$arguments = func_num_args() === 2 ? [$value] : [$value, $default];
return $this->when(! $condition, ...$arguments);
}
/**
* Merge a value into the array.
*
* @param mixed $value
* @return \Illuminate\Http\Resources\MergeValue|mixed
*/
protected function merge($value)
{
return $this->mergeWhen(true, $value);
}
/**
* Merge a value if the given condition is truthy.
*
* @param bool $condition
* @param mixed $value
* @param mixed $default
* @return \Illuminate\Http\Resources\MergeValue|mixed
*/
protected function mergeWhen($condition, $value, $default = new MissingValue)
{
if ($condition) {
return new MergeValue(value($value));
}
return func_num_args() === 3 ? new MergeValue(value($default)) : $default;
}
/**
* Merge a value unless the given condition is truthy.
*
* @param bool $condition
* @param mixed $value
* @param mixed $default
* @return \Illuminate\Http\Resources\MergeValue|mixed
*/
protected function mergeUnless($condition, $value, $default = new MissingValue)
{
$arguments = func_num_args() === 2 ? [$value] : [$value, $default];
return $this->mergeWhen(! $condition, ...$arguments);
}
/**
* Merge the given attributes.
*
* @param array $attributes
* @return \Illuminate\Http\Resources\MergeValue
*/
protected function attributes($attributes)
{
return new MergeValue(
Arr::only($this->resource->toArray(), $attributes)
);
}
/**
* Retrieve an attribute if it exists on the resource.
*
* @param string $attribute
* @param mixed $value
* @param mixed $default
* @return \Illuminate\Http\Resources\MissingValue|mixed
*/
public function whenHas($attribute, $value = null, $default = new MissingValue)
{
if (! array_key_exists($attribute, $this->resource->getAttributes())) {
return value($default);
}
return func_num_args() === 1
? $this->resource->{$attribute}
: value($value, $this->resource->{$attribute});
}
/**
* Retrieve a model attribute if it is null.
*
* @param mixed $value
* @param mixed $default
* @return \Illuminate\Http\Resources\MissingValue|mixed
*/
protected function whenNull($value, $default = new MissingValue)
{
$arguments = func_num_args() == 1 ? [$value] : [$value, $default];
return $this->when(is_null($value), ...$arguments);
}
/**
* Retrieve a model attribute if it is not null.
*
* @param mixed $value
* @param mixed $default
* @return \Illuminate\Http\Resources\MissingValue|mixed
*/
protected function whenNotNull($value, $default = new MissingValue)
{
$arguments = func_num_args() == 1 ? [$value] : [$value, $default];
return $this->when(! is_null($value), ...$arguments);
}
/**
* Retrieve an accessor when it has been appended.
*
* @param string $attribute
* @param mixed $value
* @param mixed $default
* @return \Illuminate\Http\Resources\MissingValue|mixed
*/
protected function whenAppended($attribute, $value = null, $default = new MissingValue)
{
if ($this->resource->hasAppended($attribute)) {
return func_num_args() >= 2 ? value($value) : $this->resource->$attribute;
}
return func_num_args() === 3 ? value($default) : $default;
}
/**
* Retrieve a relationship if it has been loaded.
*
* @param string $relationship
* @param mixed $value
* @param mixed $default
* @return \Illuminate\Http\Resources\MissingValue|mixed
*/
protected function whenLoaded($relationship, $value = null, $default = new MissingValue)
{
if (! $this->resource->relationLoaded($relationship)) {
return value($default);
}
$loadedValue = $this->resource->{$relationship};
if (func_num_args() === 1) {
return $loadedValue;
}
if ($loadedValue === null) {
return;
}
if ($value === null) {
$value = value(...);
}
return value($value, $loadedValue);
}
/**
* Retrieve a relationship count if it exists.
*
* @param string $relationship
* @param mixed $value
* @param mixed $default
* @return \Illuminate\Http\Resources\MissingValue|mixed
*/
public function whenCounted($relationship, $value = null, $default = new MissingValue)
{
$attribute = (new Stringable($relationship))->snake()->finish('_count')->value();
if (! array_key_exists($attribute, $this->resource->getAttributes())) {
return value($default);
}
if (func_num_args() === 1) {
return $this->resource->{$attribute};
}
if ($this->resource->{$attribute} === null) {
return;
}
if ($value === null) {
$value = value(...);
}
return value($value, $this->resource->{$attribute});
}
/**
* Retrieve a relationship aggregated value if it exists.
*
* @param string $relationship
* @param string $column
* @param string $aggregate
* @param mixed $value
* @param mixed $default
* @return \Illuminate\Http\Resources\MissingValue|mixed
*/
public function whenAggregated($relationship, $column, $aggregate, $value = null, $default = new MissingValue)
{
$attribute = (new Stringable($relationship))->snake()->append('_')->append($aggregate)->append('_')->finish($column)->value();
if (! array_key_exists($attribute, $this->resource->getAttributes())) {
return value($default);
}
if (func_num_args() === 3) {
return $this->resource->{$attribute};
}
if ($this->resource->{$attribute} === null) {
return;
}
if ($value === null) {
$value = value(...);
}
return value($value, $this->resource->{$attribute});
}
/**
* Retrieve a relationship existence check if it exists.
*
* @param string $relationship
* @param mixed $value
* @param mixed $default
* @return \Illuminate\Http\Resources\MissingValue|mixed
*/
public function whenExistsLoaded($relationship, $value = null, $default = new MissingValue)
{
$attribute = (new Stringable($relationship))->snake()->finish('_exists')->value();
if (! array_key_exists($attribute, $this->resource->getAttributes())) {
return value($default);
}
if (func_num_args() === 1) {
return $this->resource->{$attribute};
}
if ($this->resource->{$attribute} === null) {
return;
}
return value($value, $this->resource->{$attribute});
}
/**
* Execute a callback if the given pivot table has been loaded.
*
* @param string $table
* @param mixed $value
* @param mixed $default
* @return \Illuminate\Http\Resources\MissingValue|mixed
*/
protected function whenPivotLoaded($table, $value, $default = new MissingValue)
{
return $this->whenPivotLoadedAs('pivot', ...func_get_args());
}
/**
* Execute a callback if the given pivot table with a custom accessor has been loaded.
*
* @param string $accessor
* @param string $table
* @param mixed $value
* @param mixed $default
* @return \Illuminate\Http\Resources\MissingValue|mixed
*/
protected function whenPivotLoadedAs($accessor, $table, $value, $default = new MissingValue)
{
return $this->when(
$this->hasPivotLoadedAs($accessor, $table),
$value,
$default,
);
}
/**
* Determine if the resource has the specified pivot table loaded.
*
* @param string $table
* @return bool
*/
protected function hasPivotLoaded($table)
{
return $this->hasPivotLoadedAs('pivot', $table);
}
/**
* Determine if the resource has the specified pivot table loaded with a custom accessor.
*
* @param string $accessor
* @param string $table
* @return bool
*/
protected function hasPivotLoadedAs($accessor, $table)
{
return isset($this->resource->$accessor) &&
($this->resource->$accessor instanceof $table ||
$this->resource->$accessor->getTable() === $table);
}
/**
* Transform the given value if it is present.
*
* @param mixed $value
* @param callable $callback
* @param mixed $default
* @return mixed
*/
protected function transform($value, callable $callback, $default = new MissingValue)
{
return transform(
$value, $callback, $default
);
}
}

View File

@@ -0,0 +1,157 @@
<?php
namespace Illuminate\Http\Resources;
use Exception;
use Illuminate\Support\Traits\ForwardsCalls;
use Illuminate\Support\Traits\Macroable;
trait DelegatesToResource
{
use ForwardsCalls, Macroable {
__call as macroCall;
}
/**
* Get the value of the resource's route key.
*
* @return mixed
*/
public function getRouteKey()
{
return $this->resource->getRouteKey();
}
/**
* Get the route key for the resource.
*
* @return string
*/
public function getRouteKeyName()
{
return $this->resource->getRouteKeyName();
}
/**
* Retrieve the model for a bound value.
*
* @param mixed $value
* @param string|null $field
* @return void
*
* @throws \Exception
*/
public function resolveRouteBinding($value, $field = null)
{
throw new Exception('Resources may not be implicitly resolved from route bindings.');
}
/**
* Retrieve the model for a bound value.
*
* @param string $childType
* @param mixed $value
* @param string|null $field
* @return void
*
* @throws \Exception
*/
public function resolveChildRouteBinding($childType, $value, $field = null)
{
throw new Exception('Resources may not be implicitly resolved from child route bindings.');
}
/**
* Determine if the given attribute exists.
*
* @param mixed $offset
* @return bool
*/
public function offsetExists($offset): bool
{
return isset($this->resource[$offset]);
}
/**
* Get the value for a given offset.
*
* @param mixed $offset
* @return mixed
*/
public function offsetGet($offset): mixed
{
return $this->resource[$offset];
}
/**
* Set the value for a given offset.
*
* @param mixed $offset
* @param mixed $value
* @return void
*/
public function offsetSet($offset, $value): void
{
$this->resource[$offset] = $value;
}
/**
* Unset the value for a given offset.
*
* @param mixed $offset
* @return void
*/
public function offsetUnset($offset): void
{
unset($this->resource[$offset]);
}
/**
* Determine if an attribute exists on the resource.
*
* @param string $key
* @return bool
*/
public function __isset($key)
{
return isset($this->resource->{$key});
}
/**
* Unset an attribute on the resource.
*
* @param string $key
* @return void
*/
public function __unset($key)
{
unset($this->resource->{$key});
}
/**
* Dynamically get properties from the underlying resource.
*
* @param string $key
* @return mixed
*/
public function __get($key)
{
return $this->resource->{$key};
}
/**
* Dynamically pass method calls to the underlying resource.
*
* @param string $method
* @param array $parameters
* @return mixed
*/
public function __call($method, $parameters)
{
if (static::hasMacro($method)) {
return $this->macroCall($method, $parameters);
}
return $this->forwardCallTo($this->resource, $method, $parameters);
}
}

View File

@@ -0,0 +1,43 @@
<?php
namespace Illuminate\Http\Resources\Json;
class AnonymousResourceCollection extends ResourceCollection
{
/**
* The name of the resource being collected.
*
* @var string
*/
public $collects;
/**
* Indicates if the collection keys should be preserved.
*
* @var bool
*/
public $preserveKeys = false;
/**
* Create a new anonymous resource collection.
*
* @param mixed $resource
* @param string $collects
*/
public function __construct($resource, $collects)
{
$this->collects = $collects;
parent::__construct($resource);
}
/**
* Indicate that the collection keys should be preserved.
*/
public function preserveKeys(bool $value = true): static
{
$this->preserveKeys = $value;
return $this;
}
}

View File

@@ -0,0 +1,323 @@
<?php
namespace Illuminate\Http\Resources\Json;
use ArrayAccess;
use Illuminate\Container\Container;
use Illuminate\Contracts\Routing\UrlRoutable;
use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Contracts\Support\Responsable;
use Illuminate\Database\Eloquent\JsonEncodingException;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\ConditionallyLoadsAttributes;
use Illuminate\Http\Resources\DelegatesToResource;
use JsonException;
use JsonSerializable;
class JsonResource implements ArrayAccess, JsonSerializable, Responsable, UrlRoutable
{
use ConditionallyLoadsAttributes, DelegatesToResource;
/**
* The resource instance.
*
* @var mixed
*/
public $resource;
/**
* The additional data that should be added to the top-level resource array.
*
* @var array
*/
public $with = [];
/**
* The additional meta data that should be added to the resource response.
*
* Added during response construction by the developer.
*
* @var array
*/
public $additional = [];
/**
* The "data" wrapper that should be applied.
*
* @var string|null
*/
public static $wrap = 'data';
/**
* Whether to force wrapping even if the $wrap key exists in underlying resource data.
*
* @var bool
*/
public static bool $forceWrapping = false;
/**
* Create a new resource instance.
*
* @param mixed $resource
*/
public function __construct($resource)
{
$this->resource = $resource;
}
/**
* Create a new resource instance.
*
* @param mixed ...$parameters
* @return static
*/
public static function make(...$parameters)
{
return new static(...$parameters);
}
/**
* Create a new anonymous resource collection.
*
* @param mixed $resource
* @return \Illuminate\Http\Resources\Json\AnonymousResourceCollection
*/
public static function collection($resource)
{
return tap(static::newCollection($resource), function ($collection) {
if (property_exists(static::class, 'preserveKeys')) {
$collection->preserveKeys = (new static([]))->preserveKeys === true;
}
});
}
/**
* Create a new resource collection instance.
*
* @param mixed $resource
* @return \Illuminate\Http\Resources\Json\AnonymousResourceCollection
*/
protected static function newCollection($resource)
{
return new AnonymousResourceCollection($resource, static::class);
}
/**
* Resolve the resource to an array.
*
* @param \Illuminate\Http\Request|null $request
* @return array
*/
public function resolve($request = null)
{
$data = $this->resolveResourceData(
$request ?: $this->resolveRequestFromContainer()
);
if ($data instanceof Arrayable) {
$data = $data->toArray();
} elseif ($data instanceof JsonSerializable) {
$data = $data->jsonSerialize();
}
return $this->filter((array) $data);
}
/**
* Transform the resource into an array.
*
* @param \Illuminate\Http\Request $request
* @return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable
*/
public function toAttributes(Request $request)
{
if (property_exists($this, 'attributes')) {
return $this->attributes;
}
return $this->toArray($request);
}
/**
* Resolve the resource data to an array.
*
* @param \Illuminate\Http\Request $request
* @return array
*/
public function resolveResourceData(Request $request)
{
return $this->toAttributes($request);
}
/**
* Transform the resource into an array.
*
* @param \Illuminate\Http\Request $request
* @return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable
*/
public function toArray(Request $request)
{
if (is_null($this->resource)) {
return [];
}
return is_array($this->resource)
? $this->resource
: $this->resource->toArray();
}
/**
* Convert the resource to JSON.
*
* @param int $options
* @return string
*
* @throws \Illuminate\Database\Eloquent\JsonEncodingException
*/
public function toJson($options = 0)
{
try {
$json = json_encode($this->jsonSerialize(), $options | JSON_THROW_ON_ERROR);
} catch (JsonException $e) {
throw JsonEncodingException::forResource($this, $e->getMessage());
}
return $json;
}
/**
* Convert the resource to pretty print formatted JSON.
*
* @param int $options
* @return string
*
* @throws \Illuminate\Database\Eloquent\JsonEncodingException
*/
public function toPrettyJson(int $options = 0)
{
return $this->toJson(JSON_PRETTY_PRINT | $options);
}
/**
* Get any additional data that should be returned with the resource array.
*
* @param \Illuminate\Http\Request $request
* @return array
*/
public function with(Request $request)
{
return $this->with;
}
/**
* Add additional meta data to the resource response.
*
* @param array $data
* @return $this
*/
public function additional(array $data)
{
$this->additional = $data;
return $this;
}
/**
* Get the JSON serialization options that should be applied to the resource response.
*
* @return int
*/
public function jsonOptions()
{
return 0;
}
/**
* Customize the response for a request.
*
* @param \Illuminate\Http\Request $request
* @param \Illuminate\Http\JsonResponse $response
* @return void
*/
public function withResponse(Request $request, JsonResponse $response)
{
//
}
/**
* Resolve the HTTP request instance from container.
*
* @return \Illuminate\Http\Request
*/
protected function resolveRequestFromContainer()
{
return Container::getInstance()->make('request');
}
/**
* Set the string that should wrap the outer-most resource array.
*
* @param string $value
* @return void
*/
public static function wrap($value)
{
static::$wrap = $value;
}
/**
* Disable wrapping of the outer-most resource array.
*
* @return void
*/
public static function withoutWrapping()
{
static::$wrap = null;
}
/**
* Transform the resource into an HTTP response.
*
* @param \Illuminate\Http\Request|null $request
* @return \Illuminate\Http\JsonResponse
*/
public function response($request = null)
{
return $this->toResponse(
$request ?: $this->resolveRequestFromContainer()
);
}
/**
* Create an HTTP response that represents the object.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\JsonResponse
*/
public function toResponse($request)
{
return (new ResourceResponse($this))->toResponse($request);
}
/**
* Prepare the resource for JSON serialization.
*
* @return array
*/
public function jsonSerialize(): array
{
return $this->resolve($this->resolveRequestFromContainer());
}
/**
* Flush the resource's global state.
*
* @return void
*/
public static function flushState()
{
static::$wrap = 'data';
static::$forceWrapping = false;
}
}

View File

@@ -0,0 +1,99 @@
<?php
namespace Illuminate\Http\Resources\Json;
use Illuminate\Support\Arr;
class PaginatedResourceResponse extends ResourceResponse
{
/**
* Create an HTTP response that represents the object.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\JsonResponse
*/
public function toResponse($request)
{
return tap(response()->json(
$this->wrap(
$this->resource->resolve($request),
array_merge_recursive(
$this->paginationInformation($request),
$this->resource->with($request),
$this->resource->additional
)
),
$this->calculateStatus(),
[],
$this->resource->jsonOptions()
), function ($response) use ($request) {
$response->original = $this->resource->resource->map(function ($item) {
if (is_array($item)) {
return Arr::get($item, 'resource');
} elseif (is_object($item)) {
return $item->resource ?? null;
}
return null;
});
$this->resource->withResponse($request, $response);
});
}
/**
* Add the pagination information to the response.
*
* @param \Illuminate\Http\Request $request
* @return array
*/
protected function paginationInformation($request)
{
$paginated = $this->resource->resource->toArray();
$default = [
'links' => $this->paginationLinks($paginated),
'meta' => $this->meta($paginated),
];
if (method_exists($this->resource, 'paginationInformation') ||
$this->resource->hasMacro('paginationInformation')) {
return $this->resource->paginationInformation($request, $paginated, $default);
}
return $default;
}
/**
* Get the pagination links for the response.
*
* @param array $paginated
* @return array
*/
protected function paginationLinks($paginated)
{
return [
'first' => $paginated['first_page_url'] ?? null,
'last' => $paginated['last_page_url'] ?? null,
'prev' => $paginated['prev_page_url'] ?? null,
'next' => $paginated['next_page_url'] ?? null,
];
}
/**
* Gather the meta data for the response.
*
* @param array $paginated
* @return array
*/
protected function meta($paginated)
{
return Arr::except($paginated, [
'data',
'first_page_url',
'last_page_url',
'prev_page_url',
'next_page_url',
]);
}
}

View File

@@ -0,0 +1,140 @@
<?php
namespace Illuminate\Http\Resources\Json;
use Countable;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\CollectsResources;
use Illuminate\Pagination\AbstractCursorPaginator;
use Illuminate\Pagination\AbstractPaginator;
use IteratorAggregate;
class ResourceCollection extends JsonResource implements Countable, IteratorAggregate
{
use CollectsResources;
/**
* The resource that this resource collects.
*
* @var string
*/
public $collects;
/**
* The mapped collection instance.
*
* @var \Illuminate\Support\Collection|null
*/
public $collection;
/**
* Indicates if all existing request query parameters should be added to pagination links.
*
* @var bool
*/
protected $preserveAllQueryParameters = false;
/**
* The query parameters that should be added to the pagination links.
*
* @var array|null
*/
protected $queryParameters;
/**
* Create a new resource instance.
*
* @param mixed $resource
*/
public function __construct($resource)
{
parent::__construct($resource);
$this->resource = $this->collectResource($resource);
}
/**
* Indicate that all current query parameters should be appended to pagination links.
*
* @return $this
*/
public function preserveQuery()
{
$this->preserveAllQueryParameters = true;
return $this;
}
/**
* Specify the query string parameters that should be present on pagination links.
*
* @param array $query
* @return $this
*/
public function withQuery(array $query)
{
$this->preserveAllQueryParameters = false;
$this->queryParameters = $query;
return $this;
}
/**
* Return the count of items in the resource collection.
*
* @return int
*/
public function count(): int
{
return $this->collection->count();
}
/**
* Transform the resource into a JSON array.
*
* @param \Illuminate\Http\Request $request
* @return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable
*/
#[\Override]
public function toArray(Request $request)
{
if ($this->collection->first() instanceof JsonResource) {
return $this->collection->map->resolve($request)->all();
}
return $this->collection->map->toArray($request)->all();
}
/**
* Create an HTTP response that represents the object.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\JsonResponse
*/
public function toResponse($request)
{
if ($this->resource instanceof AbstractPaginator || $this->resource instanceof AbstractCursorPaginator) {
return $this->preparePaginatedResponse($request);
}
return parent::toResponse($request);
}
/**
* Create a paginate-aware HTTP response.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\JsonResponse
*/
protected function preparePaginatedResponse($request)
{
if ($this->preserveAllQueryParameters) {
$this->resource->appends($request->query());
} elseif (! is_null($this->queryParameters)) {
$this->resource->appends($this->queryParameters);
}
return (new PaginatedResourceResponse($this))->toResponse($request);
}
}

View File

@@ -0,0 +1,125 @@
<?php
namespace Illuminate\Http\Resources\Json;
use Illuminate\Contracts\Support\Responsable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
class ResourceResponse implements Responsable
{
/**
* The underlying resource.
*
* @var mixed
*/
public $resource;
/**
* Create a new resource response.
*
* @param mixed $resource
*/
public function __construct($resource)
{
$this->resource = $resource;
}
/**
* Create an HTTP response that represents the object.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\JsonResponse
*/
public function toResponse($request)
{
return tap(response()->json(
$this->wrap(
$this->resource->resolve($request),
$this->resource->with($request),
$this->resource->additional
),
$this->calculateStatus(),
[],
$this->resource->jsonOptions()
), function ($response) use ($request) {
$response->original = $this->resource->resource;
$this->resource->withResponse($request, $response);
});
}
/**
* Wrap the given data if necessary.
*
* @param \Illuminate\Support\Collection|array $data
* @param array $with
* @param array $additional
* @return array
*/
protected function wrap($data, $with = [], $additional = [])
{
if ($data instanceof Collection) {
$data = $data->all();
}
if ($this->haveDefaultWrapperAndDataIsUnwrapped($data)) {
$data = [$this->wrapper() => $data];
} elseif ($this->haveAdditionalInformationAndDataIsUnwrapped($data, $with, $additional)) {
$data = [($this->wrapper() ?? 'data') => $data];
}
return array_merge_recursive($data, $with, $additional);
}
/**
* Determine if we have a default wrapper and the given data is unwrapped.
*
* @param array $data
* @return bool
*/
protected function haveDefaultWrapperAndDataIsUnwrapped($data)
{
if ($this->resource instanceof JsonResource && $this->resource::$forceWrapping) {
return $this->wrapper() !== null;
}
return $this->wrapper() && ! array_key_exists($this->wrapper(), $data);
}
/**
* Determine if "with" data has been added and our data is unwrapped.
*
* @param array $data
* @param array $with
* @param array $additional
* @return bool
*/
protected function haveAdditionalInformationAndDataIsUnwrapped($data, $with, $additional)
{
return (! empty($with) || ! empty($additional)) &&
(! $this->wrapper() ||
! array_key_exists($this->wrapper(), $data));
}
/**
* Get the default data wrapper for the resource.
*
* @return string
*/
protected function wrapper()
{
return get_class($this->resource)::$wrap;
}
/**
* Calculate the appropriate status code for the response.
*
* @return int
*/
protected function calculateStatus()
{
return $this->resource->resource instanceof Model &&
$this->resource->resource->wasRecentlyCreated ? 201 : 200;
}
}

View File

@@ -0,0 +1,86 @@
<?php
namespace Illuminate\Http\Resources\JsonApi;
use Illuminate\Container\Container;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Arr;
class AnonymousResourceCollection extends \Illuminate\Http\Resources\Json\AnonymousResourceCollection
{
use Concerns\ResolvesJsonApiRequest;
/**
* Get any additional data that should be returned with the resource array.
*
* @param \Illuminate\Http\Request $request
* @return array
*/
#[\Override]
public function with($request)
{
return array_filter([
'included' => $this->collection
->map(fn ($resource) => $resource->resolveIncludedResourceObjects($request))
->flatten(depth: 1)
->uniqueStrict('_uniqueKey')
->map(fn ($included) => Arr::except($included, ['_uniqueKey']))
->values()
->all(),
...($implementation = JsonApiResource::$jsonApiInformation)
? ['jsonapi' => $implementation]
: [],
]);
}
/**
* Transform the resource into a JSON array.
*
* @param \Illuminate\Http\Request $request
* @return array
*/
#[\Override]
public function toAttributes(Request $request)
{
return $this->collection
->map(fn ($resource) => $resource->resolveResourceData($request))
->all();
}
/**
* Customize the outgoing response for the resource.
*
* @param \Illuminate\Http\Request $request
* @param \Illuminate\Http\JsonResponse $response
* @return void
*/
#[\Override]
public function withResponse(Request $request, JsonResponse $response): void
{
$response->header('Content-Type', 'application/vnd.api+json');
}
/**
* Create an HTTP response that represents the object.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\JsonResponse
*/
#[\Override]
public function toResponse($request)
{
return parent::toResponse($this->resolveJsonApiRequestFrom($request));
}
/**
* Resolve the HTTP request instance from container.
*
* @return \Illuminate\Http\Resources\JsonApi\SparseRequest
*/
#[\Override]
protected function resolveRequestFromContainer()
{
return $this->resolveJsonApiRequestFrom(Container::getInstance()->make('request'));
}
}

View File

@@ -0,0 +1,440 @@
<?php
namespace Illuminate\Http\Resources\JsonApi\Concerns;
use Generator;
use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\Concerns\AsPivot;
use Illuminate\Database\Eloquent\Relations\Pivot;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Http\Resources\JsonApi\Exceptions\ResourceIdentificationException;
use Illuminate\Http\Resources\JsonApi\JsonApiRequest;
use Illuminate\Http\Resources\JsonApi\JsonApiResource;
use Illuminate\Http\Resources\JsonApi\RelationResolver;
use Illuminate\Http\Resources\MissingValue;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Illuminate\Support\LazyCollection;
use Illuminate\Support\Str;
use JsonSerializable;
use WeakMap;
trait ResolvesJsonApiElements
{
/**
* Determine whether resources respect inclusions and fields from the request.
*/
protected bool $usesRequestQueryString = true;
/**
* Determine whether included relationship for the resource from eager loaded relationship.
*/
protected bool $includesPreviouslyLoadedRelationships = false;
/**
* Cached loaded relationships map.
*
* @var array<int, array{0: \Illuminate\Http\Resources\JsonApi\JsonApiResource, 1: string, 2: string, 3: bool}|null
*/
public $loadedRelationshipsMap;
/**
* Cached loaded relationships identifers.
*/
protected array $loadedRelationshipIdentifiers = [];
/**
* The maximum relationship depth.
*/
public static int $maxRelationshipDepth = 5;
/**
* Specify the maximum relationship depth.
*/
public static function maxRelationshipDepth(int $depth): void
{
static::$maxRelationshipDepth = max(0, $depth);
}
/**
* Resolves `data` for the resource.
*/
protected function resolveResourceObject(JsonApiRequest $request): array
{
$resourceType = $this->resolveResourceType($request);
return [
'id' => $this->resolveResourceIdentifier($request),
'type' => $resourceType,
...(new Collection([
'attributes' => $this->resolveResourceAttributes($request, $resourceType),
'relationships' => $this->resolveResourceRelationshipIdentifiers($request),
'links' => $this->resolveResourceLinks($request),
'meta' => $this->resolveResourceMetaInformation($request),
]))->filter()->map(fn ($value) => (object) $value),
];
}
/**
* Resolve the resource's identifier.
*
* @return string|int
*
* @throws ResourceIdentificationException
*/
public function resolveResourceIdentifier(JsonApiRequest $request): string
{
if (! is_null($resourceId = $this->toId($request))) {
return $resourceId;
}
if (! ($this->resource instanceof Model || method_exists($this->resource, 'getKey'))) {
throw ResourceIdentificationException::attemptingToDetermineIdFor($this);
}
return (string) $this->resource->getKey();
}
/**
* Resolve the resource's type.
*
*
* @throws ResourceIdentificationException
*/
public function resolveResourceType(JsonApiRequest $request): string
{
if (! is_null($resourceType = $this->toType($request))) {
return $resourceType;
}
if (static::class !== JsonApiResource::class) {
return Str::of(static::class)->classBasename()->basename('Resource')->snake()->pluralStudly();
}
if (! $this->resource instanceof Model) {
throw ResourceIdentificationException::attemptingToDetermineTypeFor($this);
}
$modelClassName = $this->resource::class;
$morphMap = Relation::getMorphAlias($modelClassName);
return Str::of(
$morphMap !== $modelClassName ? $morphMap : class_basename($modelClassName)
)->snake()->pluralStudly();
}
/**
* Resolve the resource's attributes.
*
*
* @throws \RuntimeException
*/
protected function resolveResourceAttributes(JsonApiRequest $request, string $resourceType): array
{
$data = $this->toAttributes($request);
if ($data instanceof Arrayable) {
$data = $data->toArray();
} elseif ($data instanceof JsonSerializable) {
$data = $data->jsonSerialize();
}
$sparseFieldset = match ($this->usesRequestQueryString) {
true => $request->sparseFields($resourceType),
default => [],
};
$data = (new Collection($data))
->mapWithKeys(fn ($value, $key) => is_int($key) ? [$value => $this->resource->{$value}] : [$key => $value])
->when(! empty($sparseFieldset), fn ($attributes) => $attributes->only($sparseFieldset))
->transform(fn ($value) => value($value, $request))
->all();
return $this->filter($data);
}
/**
* Resolves `relationships` for the resource's data object.
*
* @return string|int
*
* @throws \RuntimeException
*/
protected function resolveResourceRelationshipIdentifiers(JsonApiRequest $request): array
{
if (! $this->resource instanceof Model) {
return [];
}
$this->compileResourceRelationships($request);
return [
...(new Collection($this->filter($this->loadedRelationshipIdentifiers)))
->map(function ($relation) {
return ! is_null($relation) ? $relation : ['data' => null];
})->all(),
];
}
/**
* Compile resource relationships.
*/
protected function compileResourceRelationships(JsonApiRequest $request): void
{
if (! is_null($this->loadedRelationshipsMap)) {
return;
}
$sparseIncluded = match (true) {
$this->includesPreviouslyLoadedRelationships => array_keys($this->resource->getRelations()),
default => $request->sparseIncluded(),
};
$resourceRelationships = (new Collection($this->toRelationships($request)))
->transform(fn ($value, $key) => is_int($key) ? new RelationResolver($value) : new RelationResolver($key, $value))
->mapWithKeys(fn ($relationResolver) => [$relationResolver->relationName => $relationResolver])
->filter(fn ($value, $key) => in_array($key, $sparseIncluded));
$resourceRelationshipKeys = $resourceRelationships->keys();
$this->resource->loadMissing($resourceRelationshipKeys->all() ?? []);
$this->loadedRelationshipsMap = [];
$this->loadedRelationshipIdentifiers = (new LazyCollection(function () use ($request, $resourceRelationships) {
foreach ($resourceRelationships as $relationName => $relationResolver) {
$relatedModels = $relationResolver->handle($this->resource);
$relatedResourceClass = $relationResolver->resourceClass();
if (! is_null($relatedModels) && $this->includesPreviouslyLoadedRelationships === false) {
if (! empty($relations = $request->sparseIncluded($relationName))) {
$relatedModels->loadMissing($relations);
}
}
yield from $this->compileResourceRelationshipUsingResolver(
$request,
$this->resource,
$relationResolver,
$relatedModels,
);
}
}))->all();
}
/**
* Compile resource relations.
*/
protected function compileResourceRelationshipUsingResolver(
JsonApiRequest $request,
mixed $resource,
RelationResolver $relationResolver,
Collection|Model|null $relatedModels
): Generator {
$relationName = $relationResolver->relationName;
$resourceClass = $relationResolver->resourceClass();
// Relationship is a collection of models...
if ($relatedModels instanceof Collection) {
$relatedModels = $relatedModels->values();
if ($relatedModels->isEmpty()) {
yield $relationName => ['data' => $relatedModels];
return;
}
$relationship = $resource->{$relationName}();
$isUnique = ! $relationship instanceof BelongsToMany;
yield $relationName => ['data' => $relatedModels->map(function ($relatedModel) use ($request, $resourceClass, $isUnique) {
$relatedResource = rescue(fn () => $relatedModel->toResource($resourceClass), new JsonApiResource($relatedModel));
return transform(
[$relatedResource->resolveResourceType($request), $relatedResource->resolveResourceIdentifier($request)],
function ($uniqueKey) use ($request, $relatedModel, $relatedResource, $isUnique) {
$this->loadedRelationshipsMap[] = [$relatedResource, ...$uniqueKey, $isUnique];
$this->compileIncludedNestedRelationshipsMap($request, $relatedModel, $relatedResource);
return [
'id' => $uniqueKey[1],
'type' => $uniqueKey[0],
];
}
);
})->all()];
return;
}
// Relationship is a single model...
$relatedModel = $relatedModels;
if (is_null($relatedModel)) {
yield $relationName => null;
return;
} elseif ($relatedModel instanceof Pivot ||
in_array(AsPivot::class, class_uses_recursive($relatedModel), true)) {
yield $relationName => new MissingValue;
return;
}
$relatedResource = rescue(fn () => $relatedModel->toResource($resourceClass), new JsonApiResource($relatedModel));
yield $relationName => ['data' => transform(
[$relatedResource->resolveResourceType($request), $relatedResource->resolveResourceIdentifier($request)],
function ($uniqueKey) use ($relatedModel, $relatedResource, $request) {
$this->loadedRelationshipsMap[] = [$relatedResource, ...$uniqueKey, true];
$this->compileIncludedNestedRelationshipsMap($request, $relatedModel, $relatedResource);
return [
'id' => $uniqueKey[1],
'type' => $uniqueKey[0],
];
}
)];
}
/**
* Compile included relationships map.
*/
protected function compileIncludedNestedRelationshipsMap(JsonApiRequest $request, Model $relation, JsonApiResource $resource): void
{
(new Collection($resource->toRelationships($request)))
->transform(fn ($value, $key) => is_int($key) ? new RelationResolver($value) : new RelationResolver($key, $value))
->mapWithKeys(fn ($relationResolver) => [$relationResolver->relationName => $relationResolver])
->filter(fn ($value, $key) => in_array($key, array_keys($relation->getRelations())))
->each(function ($relationResolver, $key) use ($relation, $request) {
$this->compileResourceRelationshipUsingResolver($request, $relation, $relationResolver, $relation->getRelation($key));
});
}
/**
* Resolves `included` for the resource.
*/
public function resolveIncludedResourceObjects(JsonApiRequest $request): Collection
{
if (! $this->resource instanceof Model) {
return [];
}
$this->compileResourceRelationships($request);
$relations = new Collection;
$index = 0;
// Track visited objects by instance + type to prevent infinite loops from circular
// references created by "chaperone()". We use object instances rather than type
// and ID for any possible cases like BelongsToMany with different pivot data.
// We'll track types to allow the same models with different resource types.
$visitedObjects = new WeakMap;
$visitedObjects[$this->resource] = [
$this->resolveResourceType($request) => true,
];
while ($index < count($this->loadedRelationshipsMap)) {
[$resourceInstance, $type, $id, $isUnique] = $this->loadedRelationshipsMap[$index];
$underlyingResource = $resourceInstance->resource;
if (is_object($underlyingResource)) {
if (isset($visitedObjects[$underlyingResource][$type])) {
$index++;
continue;
}
$visitedObjects[$underlyingResource] ??= [];
$visitedObjects[$underlyingResource][$type] = true;
}
if (! $resourceInstance instanceof JsonApiResource &&
$resourceInstance instanceof JsonResource) {
$resourceInstance = new JsonApiResource($resourceInstance->resource);
}
$relationsData = $resourceInstance
->includePreviouslyLoadedRelationships()
->resolve($request);
array_push($this->loadedRelationshipsMap, ...($resourceInstance->loadedRelationshipsMap ?? []));
$relations->push(array_filter([
'id' => $id,
'type' => $type,
'_uniqueKey' => implode(':', $isUnique === true ? [$id, $type] : [$id, $type, (string) Str::random()]),
'attributes' => Arr::get($relationsData, 'data.attributes'),
'relationships' => Arr::get($relationsData, 'data.relationships'),
'links' => Arr::get($relationsData, 'data.links'),
'meta' => Arr::get($relationsData, 'data.meta'),
]));
$index++;
}
return $relations;
}
/**
* Resolve the links for the resource.
*
* @return array<string, mixed>
*/
protected function resolveResourceLinks(JsonApiRequest $request): array
{
return $this->toLinks($request);
}
/**
* Resolve the meta information for the resource.
*
* @return array<string, mixed>
*/
protected function resolveResourceMetaInformation(JsonApiRequest $request): array
{
return $this->toMeta($request);
}
/**
* Indicate that relationship loading should respect the request's "includes" query string.
*
* @return $this
*/
public function respectFieldsAndIncludesInQueryString(bool $value = true)
{
$this->usesRequestQueryString = $value;
return $this;
}
/**
* Indicate that relationship loading should not rely on the request's "includes" query string.
*
* @return $this
*/
public function ignoreFieldsAndIncludesInQueryString()
{
return $this->respectFieldsAndIncludesInQueryString(false);
}
/**
* Determine relationship should include loaded relationships.
*
* @return $this
*/
public function includePreviouslyLoadedRelationships()
{
$this->includesPreviouslyLoadedRelationships = true;
return $this;
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace Illuminate\Http\Resources\JsonApi\Concerns;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\JsonApi\JsonApiRequest;
trait ResolvesJsonApiRequest
{
/**
* Resolve a JSON API request instance from the given HTTP request.
*
* @return \Illuminate\Http\Resources\JsonApi\JsonApiRequest
*/
protected function resolveJsonApiRequestFrom(Request $request)
{
return $request instanceof JsonApiRequest
? $request
: JsonApiRequest::createFrom($request);
}
}

View File

@@ -0,0 +1,38 @@
<?php
namespace Illuminate\Http\Resources\JsonApi\Exceptions;
use RuntimeException;
class ResourceIdentificationException extends RuntimeException
{
/**
* Create an exception indicating we were unable to determine the resource ID for the given resource.
*
* @param mixed $resource
* @return self
*/
public static function attemptingToDetermineIdFor($resource)
{
$resourceType = is_object($resource) ? $resource::class : gettype($resource);
return new self(sprintf(
'Unable to resolve resource object ID for [%s].', $resourceType
));
}
/**
* Create an exception indicating we were unable to determine the resource type for the given resource.
*
* @param mixed $resource
* @return self
*/
public static function attemptingToDetermineTypeFor($resource)
{
$resourceType = is_object($resource) ? $resource::class : gettype($resource);
return new self(sprintf(
'Unable to resolve resource object type for [%s].', $resourceType
));
}
}

View File

@@ -0,0 +1,71 @@
<?php
namespace Illuminate\Http\Resources\JsonApi;
use Illuminate\Http\Request;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
class JsonApiRequest extends Request
{
/**
* Cached sparse fieldset.
*/
protected ?array $cachedSparseFields = null;
/**
* Cached sparse included.
*/
protected ?array $cachedSparseIncluded = null;
/**
* Get the request's included fields.
*/
public function sparseFields(string $key): array
{
if (is_null($this->cachedSparseFields)) {
$this->cachedSparseFields = (new Collection($this->array('fields')))
->transform(fn ($fieldsets) => empty($fieldsets) ? [] : explode(',', $fieldsets))
->all();
}
return $this->cachedSparseFields[$key] ?? [];
}
/**
* Get the request's included relationships.
*/
public function sparseIncluded(?string $key = null): ?array
{
if (is_null($this->cachedSparseIncluded)) {
$included = (string) $this->string('include', '');
$this->cachedSparseIncluded = (new Collection(empty($included) ? [] : explode(',', $included)))
->transform(function ($item) {
$with = null;
if (str_contains($item, '.')) {
[$relation, $with] = explode('.', $item, 2);
} else {
$relation = $item;
}
return ['relation' => $relation, 'with' => $with];
})->mapToGroups(fn ($item) => [$item['relation'] => $item['with']])
->toArray();
}
if (is_null($key)) {
return array_keys($this->cachedSparseIncluded);
}
return transform($this->cachedSparseIncluded[$key] ?? null, function ($value) {
return (new Collection(Arr::wrap($value)))
->transform(function ($item) {
$item = implode('.', Arr::take(explode('.', $item), JsonApiResource::$maxRelationshipDepth - 1));
return ! empty($item) ? $item : null;
})->filter()->all();
}) ?? [];
}
}

View File

@@ -0,0 +1,255 @@
<?php
namespace Illuminate\Http\Resources\JsonApi;
use BadMethodCallException;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Support\Arr;
class JsonApiResource extends JsonResource
{
use Concerns\ResolvesJsonApiElements,
Concerns\ResolvesJsonApiRequest;
/**
* The "data" wrapper that should be applied.
*
* @var string|null
*/
public static $wrap = 'data';
/**
* The resource's "version" for JSON:API.
*
* @var array{version?: string, ext?: array, profile?: array, meta?: array}
*/
public static $jsonApiInformation = [];
/**
* The resource's "links" for JSON:API.
*/
protected array $jsonApiLinks = [];
/**
* The resource's "meta" for JSON:API.
*/
protected array $jsonApiMeta = [];
/**
* Set the JSON:API version for the request.
*
* @return void
*/
public static function configure(?string $version = null, array $ext = [], array $profile = [], array $meta = [])
{
static::$jsonApiInformation = array_filter([
'version' => $version,
'ext' => $ext,
'profile' => $profile,
'meta' => $meta,
]);
}
/**
* Get the resource's ID.
*
* @return string|null
*/
public function toId(Request $request)
{
return null;
}
/**
* Get the resource's type.
*
* @return string|null
*/
public function toType(Request $request)
{
return null;
}
/**
* Transform the resource into an array.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Contracts\Support\Arrayable|\JsonSerializable|array
*/
#[\Override]
public function toAttributes(Request $request)
{
if (property_exists($this, 'attributes')) {
return $this->attributes;
}
return $this->toArray($request);
}
/**
* Get the resource's relationships.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Contracts\Support\Arrayable|array
*/
public function toRelationships(Request $request)
{
if (property_exists($this, 'relationships')) {
return $this->relationships;
}
return [];
}
/**
* Get the resource's links.
*
* @return array
*/
public function toLinks(Request $request)
{
return $this->jsonApiLinks;
}
/**
* Get the resource's meta information.
*
* @return array
*/
public function toMeta(Request $request)
{
return $this->jsonApiMeta;
}
/**
* Get any additional data that should be returned with the resource array.
*
* @param \Illuminate\Http\Request $request
* @return array
*/
#[\Override]
public function with($request)
{
return array_filter([
'included' => $this->resolveIncludedResourceObjects($request)
->uniqueStrict('_uniqueKey')
->map(fn ($included) => Arr::except($included, ['_uniqueKey']))
->values()
->all(),
...($implementation = static::$jsonApiInformation)
? ['jsonapi' => $implementation]
: [],
]);
}
/**
* Resolve the resource to an array.
*
* @param \Illuminate\Http\Request|null $request
* @return array
*/
#[\Override]
public function resolve($request = null)
{
return [
'data' => $this->resolveResourceData($this->resolveJsonApiRequestFrom($request ?? $this->resolveRequestFromContainer())),
];
}
/**
* Resolve the resource data to an array.
*
* @param \Illuminate\Http\Request $request
* @return array
*/
#[\Override]
public function resolveResourceData(Request $request)
{
return $this->resolveResourceObject($request);
}
/**
* Customize the outgoing response for the resource.
*/
#[\Override]
public function withResponse(Request $request, JsonResponse $response): void
{
$response->header('Content-Type', 'application/vnd.api+json');
}
/**
* Create an HTTP response that represents the object.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\JsonResponse
*/
#[\Override]
public function toResponse($request)
{
return parent::toResponse($this->resolveJsonApiRequestFrom($request));
}
/**
* Resolve the HTTP request instance from container.
*
* @return \Illuminate\Http\Resources\JsonApi\JsonApiRequest
*/
#[\Override]
protected function resolveRequestFromContainer()
{
return $this->resolveJsonApiRequestFrom(parent::resolveRequestFromContainer());
}
/**
* Create a new resource collection instance.
*
* @param mixed $resource
* @return \Illuminate\Http\Resources\JsonApi\AnonymousResourceCollection
*/
#[\Override]
protected static function newCollection($resource)
{
return new AnonymousResourceCollection($resource, static::class);
}
/**
* Set the string that should wrap the outer-most resource array.
*
* @param string $value
* @return never
*
* @throws \RuntimeException
*/
#[\Override]
public static function wrap($value)
{
throw new BadMethodCallException(sprintf('Using %s() method is not allowed.', __METHOD__));
}
/**
* Disable wrapping of the outer-most resource array.
*
* @return never
*/
#[\Override]
public static function withoutWrapping()
{
throw new BadMethodCallException(sprintf('Using %s() method is not allowed.', __METHOD__));
}
/**
* Flush the resource's global state.
*
* @return void
*/
#[\Override]
public static function flushState()
{
parent::flushState();
static::$jsonApiInformation = [];
static::$maxRelationshipDepth = 3;
}
}

View File

@@ -0,0 +1,62 @@
<?php
namespace Illuminate\Http\Resources\JsonApi;
use Closure;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;
/**
* @internal
*/
class RelationResolver
{
/**
* The relation resolver.
*
* @var \Closure(mixed):(\Illuminate\Database\Eloquent\Collection|\Illuminate\Database\Eloquent\Model|null)
*/
public Closure $relationResolver;
/**
* The relation resource class.
*
* @var class-string<\Illuminate\Http\Resources\JsonApi\JsonApiResource>|null
*/
public ?string $relationResourceClass = null;
/**
* Construct a new resource relationship resolver.
*
* @param \Closure(mixed):(\Illuminate\Database\Eloquent\Collection|\Illuminate\Database\Eloquent\Model|null)|class-string<\Illuminate\Http\Resources\JsonApi\JsonApiResource>|null $resolver
*/
public function __construct(public string $relationName, Closure|string|null $resolver = null)
{
$this->relationResolver = match (true) {
$resolver instanceof Closure => $resolver,
default => fn ($resource) => $resource->getRelation($this->relationName),
};
if (is_string($resolver) && class_exists($resolver)) {
$this->relationResourceClass = $resolver;
}
}
/**
* Resolve the relation for a resource.
*/
public function handle(mixed $resource): Collection|Model|null
{
return value($this->relationResolver, $resource);
}
/**
* Get the resource class.
*
* @return class-string<\Illuminate\Http\Resources\JsonApi\JsonApiResource>|null
*/
public function resourceClass(): ?string
{
return $this->relationResourceClass;
}
}

View File

@@ -0,0 +1,32 @@
<?php
namespace Illuminate\Http\Resources;
use Illuminate\Support\Collection;
use JsonSerializable;
class MergeValue
{
/**
* The data to be merged.
*
* @var array
*/
public $data;
/**
* Create a new merge value instance.
*
* @param \Illuminate\Support\Collection|\JsonSerializable|array $data
*/
public function __construct($data)
{
if ($data instanceof Collection) {
$this->data = $data->all();
} elseif ($data instanceof JsonSerializable) {
$this->data = $data->jsonSerialize();
} else {
$this->data = $data;
}
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace Illuminate\Http\Resources;
class MissingValue implements PotentiallyMissing
{
/**
* Determine if the object should be considered "missing".
*
* @return bool
*/
public function isMissing()
{
return true;
}
}

View File

@@ -0,0 +1,13 @@
<?php
namespace Illuminate\Http\Resources;
interface PotentiallyMissing
{
/**
* Determine if the object should be considered "missing".
*
* @return bool
*/
public function isMissing();
}

View File

@@ -0,0 +1,117 @@
<?php
namespace Illuminate\Http;
use ArrayObject;
use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Contracts\Support\Jsonable;
use Illuminate\Contracts\Support\Renderable;
use Illuminate\Support\Traits\Macroable;
use InvalidArgumentException;
use JsonSerializable;
use Symfony\Component\HttpFoundation\Response as SymfonyResponse;
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
class Response extends SymfonyResponse
{
use ResponseTrait, Macroable {
Macroable::__call as macroCall;
}
/**
* Create a new HTTP response.
*
* @param mixed $content
* @param int $status
* @param array $headers
*
* @throws \InvalidArgumentException
*/
public function __construct($content = '', $status = 200, array $headers = [])
{
$this->headers = new ResponseHeaderBag($headers);
$this->setContent($content);
$this->setStatusCode($status);
$this->setProtocolVersion('1.0');
}
/**
* Get the response content.
*/
#[\Override]
public function getContent(): string|false
{
return transform(parent::getContent(), fn ($content) => $content, '');
}
/**
* Set the content on the response.
*
* @param mixed $content
* @return $this
*
* @throws \InvalidArgumentException
*/
#[\Override]
public function setContent(mixed $content): static
{
$this->original = $content;
// If the content is "JSONable" we will set the appropriate header and convert
// the content to JSON. This is useful when returning something like models
// from routes that will be automatically transformed to their JSON form.
if ($this->shouldBeJson($content)) {
$this->header('Content-Type', 'application/json');
$content = $this->morphToJson($content);
if ($content === false) {
throw new InvalidArgumentException(json_last_error_msg());
}
}
// If this content implements the "Renderable" interface then we will call the
// render method on the object so we will avoid any "__toString" exceptions
// that might be thrown and have their errors obscured by PHP's handling.
elseif ($content instanceof Renderable) {
$content = $content->render();
}
parent::setContent($content);
return $this;
}
/**
* Determine if the given content should be turned into JSON.
*
* @param mixed $content
* @return bool
*/
protected function shouldBeJson($content)
{
return $content instanceof Arrayable ||
$content instanceof Jsonable ||
$content instanceof ArrayObject ||
$content instanceof JsonSerializable ||
is_array($content);
}
/**
* Morph the given content into JSON.
*
* @param mixed $content
* @return string|false
*/
protected function morphToJson($content)
{
if ($content instanceof Jsonable) {
return $content->toJson();
} elseif ($content instanceof Arrayable) {
return json_encode($content->toArray());
}
return json_encode($content);
}
}

View File

@@ -0,0 +1,197 @@
<?php
namespace Illuminate\Http;
use Illuminate\Http\Exceptions\HttpResponseException;
use Symfony\Component\HttpFoundation\HeaderBag;
use Throwable;
trait ResponseTrait
{
/**
* The original content of the response.
*
* @var mixed
*/
public $original;
/**
* The exception that triggered the error response (if applicable).
*
* @var \Throwable|null
*/
public $exception;
/**
* Get the status code for the response.
*
* @return int
*/
public function status()
{
return $this->getStatusCode();
}
/**
* Get the status text for the response.
*
* @return string
*/
public function statusText()
{
return $this->statusText;
}
/**
* Get the content of the response.
*
* @return string
*/
public function content()
{
return $this->getContent();
}
/**
* Get the original response content.
*
* @return mixed
*/
public function getOriginalContent()
{
$original = $this->original;
return $original instanceof self ? $original->{__FUNCTION__}() : $original;
}
/**
* Set a header on the Response.
*
* @param string $key
* @param array|string $values
* @param bool $replace
* @return $this
*/
public function header($key, $values, $replace = true)
{
$this->headers->set($key, $values, $replace);
return $this;
}
/**
* Add an array of headers to the response.
*
* @param \Symfony\Component\HttpFoundation\HeaderBag|array $headers
* @return $this
*/
public function withHeaders($headers)
{
if ($headers instanceof HeaderBag) {
$headers = $headers->all();
}
foreach ($headers as $key => $value) {
$this->headers->set($key, $value);
}
return $this;
}
/**
* Remove a header(s) from the response.
*
* @param array|string $key
* @return $this
*/
public function withoutHeader($key)
{
foreach ((array) $key as $header) {
$this->headers->remove($header);
}
return $this;
}
/**
* Add a cookie to the response.
*
* @param \Symfony\Component\HttpFoundation\Cookie|mixed $cookie
* @return $this
*/
public function cookie($cookie)
{
return $this->withCookie(...func_get_args());
}
/**
* Add a cookie to the response.
*
* @param \Symfony\Component\HttpFoundation\Cookie|mixed $cookie
* @return $this
*/
public function withCookie($cookie)
{
if (is_string($cookie) && function_exists('cookie')) {
$cookie = cookie(...func_get_args());
}
$this->headers->setCookie($cookie);
return $this;
}
/**
* Expire a cookie when sending the response.
*
* @param \Symfony\Component\HttpFoundation\Cookie|mixed $cookie
* @param string|null $path
* @param string|null $domain
* @return $this
*/
public function withoutCookie($cookie, $path = null, $domain = null)
{
if (is_string($cookie) && function_exists('cookie')) {
$cookie = cookie($cookie, null, -2628000, $path, $domain);
}
$this->headers->setCookie($cookie);
return $this;
}
/**
* Get the callback of the response.
*
* @return string|null
*/
public function getCallback()
{
return $this->callback ?? null;
}
/**
* Set the exception to attach to the response.
*
* @param \Throwable $e
* @return $this
*/
public function withException(Throwable $e)
{
$this->exception = $e;
return $this;
}
/**
* Throws the response in a HttpResponseException instance.
*
* @return never
*
* @throws \Illuminate\Http\Exceptions\HttpResponseException
*/
public function throwResponse()
{
throw new HttpResponseException($this);
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace Illuminate\Http;
class StreamedEvent
{
/**
* The name of the event.
*/
public string $event;
/**
* The data of the stream.
*/
public mixed $data;
/**
* Create a new streamed event instance.
*/
public function __construct(string $event, mixed $data)
{
$this->event = $event;
$this->data = $data;
}
}

View File

@@ -0,0 +1,146 @@
<?php
namespace Illuminate\Http\Testing;
use Illuminate\Http\UploadedFile;
class File extends UploadedFile
{
/**
* The name of the file.
*
* @var string
*/
public $name;
/**
* The temporary file resource.
*
* @var resource
*/
public $tempFile;
/**
* The "size" to report.
*
* @var int
*/
public $sizeToReport;
/**
* The MIME type to report.
*
* @var string|null
*/
public $mimeTypeToReport;
/**
* Create a new file instance.
*
* @param string $name
* @param resource $tempFile
*/
public function __construct($name, $tempFile)
{
$this->name = $name;
$this->tempFile = $tempFile;
parent::__construct(
$this->tempFilePath(), $name, $this->getMimeType(),
null, true
);
}
/**
* Create a new fake file.
*
* @param string $name
* @param string|int $kilobytes
* @return \Illuminate\Http\Testing\File
*/
public static function create($name, $kilobytes = 0)
{
return (new FileFactory)->create($name, $kilobytes);
}
/**
* Create a new fake file with content.
*
* @param string $name
* @param string $content
* @return \Illuminate\Http\Testing\File
*/
public static function createWithContent($name, $content)
{
return (new FileFactory)->createWithContent($name, $content);
}
/**
* Create a new fake image.
*
* @param string $name
* @param int $width
* @param int $height
* @return \Illuminate\Http\Testing\File
*/
public static function image($name, $width = 10, $height = 10)
{
return (new FileFactory)->image($name, $width, $height);
}
/**
* Set the "size" of the file in kilobytes.
*
* @param int $kilobytes
* @return $this
*/
public function size($kilobytes)
{
$this->sizeToReport = $kilobytes * 1024;
return $this;
}
/**
* Get the size of the file.
*
* @return int
*/
public function getSize(): int
{
return $this->sizeToReport ?: parent::getSize();
}
/**
* Set the MIME type for the file.
*
* @param string $mimeType
* @return $this
*/
public function mimeType($mimeType)
{
$this->mimeTypeToReport = $mimeType;
return $this;
}
/**
* Get the MIME type of the file.
*
* @return string
*/
public function getMimeType(): string
{
return $this->mimeTypeToReport ?: MimeType::from($this->name);
}
/**
* Get the path to the temporary file.
*
* @return string
*/
protected function tempFilePath()
{
return stream_get_meta_data($this->tempFile)['uri'];
}
}

View File

@@ -0,0 +1,100 @@
<?php
namespace Illuminate\Http\Testing;
use LogicException;
class FileFactory
{
/**
* Create a new fake file.
*
* @param string $name
* @param string|int $kilobytes
* @param string|null $mimeType
* @return \Illuminate\Http\Testing\File
*/
public function create($name, $kilobytes = 0, $mimeType = null)
{
if (is_string($kilobytes)) {
return $this->createWithContent($name, $kilobytes);
}
return tap(new File($name, tmpfile()), function ($file) use ($kilobytes, $mimeType) {
$file->sizeToReport = $kilobytes * 1024;
$file->mimeTypeToReport = $mimeType;
});
}
/**
* Create a new fake file with content.
*
* @param string $name
* @param string $content
* @return \Illuminate\Http\Testing\File
*/
public function createWithContent($name, $content)
{
$tmpfile = tmpfile();
fwrite($tmpfile, $content);
return tap(new File($name, $tmpfile), function ($file) use ($tmpfile) {
$file->sizeToReport = fstat($tmpfile)['size'];
});
}
/**
* Create a new fake image.
*
* @param string $name
* @param int $width
* @param int $height
* @return \Illuminate\Http\Testing\File
*
* @throws \LogicException
*/
public function image($name, $width = 10, $height = 10)
{
return new File($name, $this->generateImage(
$width, $height, pathinfo($name, PATHINFO_EXTENSION)
));
}
/**
* Generate a dummy image of the given width and height.
*
* @param int $width
* @param int $height
* @param string $extension
* @return resource
*
* @throws \LogicException
*/
protected function generateImage($width, $height, $extension)
{
if (! function_exists('imagecreatetruecolor')) {
throw new LogicException('GD extension is not installed.');
}
return tap(tmpfile(), function ($temp) use ($width, $height, $extension) {
ob_start();
$extension = in_array($extension, ['jpeg', 'png', 'gif', 'webp', 'wbmp', 'bmp'])
? strtolower($extension)
: 'jpeg';
$image = imagecreatetruecolor($width, $height);
if (! function_exists($functionName = "image{$extension}")) {
ob_get_clean();
throw new LogicException("{$functionName} function is not defined and image cannot be generated.");
}
call_user_func($functionName, $image);
fwrite($temp, ob_get_clean());
});
}
}

View File

@@ -0,0 +1,65 @@
<?php
namespace Illuminate\Http\Testing;
use Illuminate\Support\Arr;
use Symfony\Component\Mime\MimeTypes;
class MimeType
{
/**
* The MIME types instance.
*
* @var \Symfony\Component\Mime\MimeTypes|null
*/
private static $mime;
/**
* Get the MIME types instance.
*
* @return \Symfony\Component\Mime\MimeTypesInterface
*/
public static function getMimeTypes()
{
if (self::$mime === null) {
self::$mime = new MimeTypes;
}
return self::$mime;
}
/**
* Get the MIME type for a file based on the file's extension.
*
* @param string $filename
* @return string
*/
public static function from($filename)
{
$extension = pathinfo($filename, PATHINFO_EXTENSION);
return self::get($extension);
}
/**
* Get the MIME type for a given extension or return all MIME types.
*
* @param string $extension
* @return string
*/
public static function get($extension)
{
return Arr::first(self::getMimeTypes()->getMimeTypes($extension)) ?? 'application/octet-stream';
}
/**
* Search for the extension of a given MIME type.
*
* @param string $mimeType
* @return string|null
*/
public static function search($mimeType)
{
return Arr::first(self::getMimeTypes()->getExtensions($mimeType));
}
}

View File

@@ -0,0 +1,157 @@
<?php
namespace Illuminate\Http;
use Illuminate\Container\Container;
use Illuminate\Contracts\Filesystem\Factory as FilesystemFactory;
use Illuminate\Contracts\Filesystem\FileNotFoundException;
use Illuminate\Http\Testing\FileFactory;
use Illuminate\Support\Arr;
use Illuminate\Support\Traits\Macroable;
use Symfony\Component\HttpFoundation\File\UploadedFile as SymfonyUploadedFile;
class UploadedFile extends SymfonyUploadedFile
{
use FileHelpers, Macroable;
/**
* Begin creating a new file fake.
*
* @return \Illuminate\Http\Testing\FileFactory
*/
public static function fake()
{
return new FileFactory;
}
/**
* Store the uploaded file on a filesystem disk.
*
* @param string $path
* @param array|string $options
* @return string|false
*/
public function store($path = '', $options = [])
{
return $this->storeAs($path, $this->hashName(), $this->parseOptions($options));
}
/**
* Store the uploaded file on a filesystem disk with public visibility.
*
* @param string $path
* @param array|string $options
* @return string|false
*/
public function storePublicly($path = '', $options = [])
{
$options = $this->parseOptions($options);
$options['visibility'] = 'public';
return $this->storeAs($path, $this->hashName(), $options);
}
/**
* Store the uploaded file on a filesystem disk with public visibility.
*
* @param string $path
* @param array|string|null $name
* @param array|string $options
* @return string|false
*/
public function storePubliclyAs($path, $name = null, $options = [])
{
if (is_null($name) || is_array($name)) {
[$path, $name, $options] = ['', $path, $name ?? []];
}
$options = $this->parseOptions($options);
$options['visibility'] = 'public';
return $this->storeAs($path, $name, $options);
}
/**
* Store the uploaded file on a filesystem disk.
*
* @param string $path
* @param array|string|null $name
* @param array|string $options
* @return string|false
*/
public function storeAs($path, $name = null, $options = [])
{
if (is_null($name) || is_array($name)) {
[$path, $name, $options] = ['', $path, $name ?? []];
}
$options = $this->parseOptions($options);
$disk = Arr::pull($options, 'disk');
return Container::getInstance()->make(FilesystemFactory::class)->disk($disk)->putFileAs(
$path, $this, $name, $options
);
}
/**
* Get the contents of the uploaded file.
*
* @return false|string
*
* @throws \Illuminate\Contracts\Filesystem\FileNotFoundException
*/
public function get()
{
if (! $this->isValid()) {
throw new FileNotFoundException("File does not exist at path {$this->getPathname()}.");
}
return file_get_contents($this->getPathname());
}
/**
* Get the file's extension supplied by the client.
*
* @return string
*/
public function clientExtension()
{
return $this->guessClientExtension();
}
/**
* Create a new file instance from a base instance.
*
* @param \Symfony\Component\HttpFoundation\File\UploadedFile $file
* @param bool $test
* @return static
*/
public static function createFromBase(SymfonyUploadedFile $file, $test = false)
{
return $file instanceof static ? $file : new static(
$file->getPathname(),
$file->getClientOriginalPath(),
$file->getClientMimeType(),
$file->getError(),
$test
);
}
/**
* Parse and format the given options.
*
* @param array|string $options
* @return array
*/
protected function parseOptions($options)
{
if (is_string($options)) {
$options = ['disk' => $options];
}
return $options;
}
}

View File

@@ -0,0 +1,49 @@
{
"name": "illuminate/http",
"description": "The Illuminate Http package.",
"license": "MIT",
"homepage": "https://laravel.com",
"support": {
"issues": "https://github.com/laravel/framework/issues",
"source": "https://github.com/laravel/framework"
},
"authors": [
{
"name": "Taylor Otwell",
"email": "taylor@laravel.com"
}
],
"require": {
"php": "^8.2",
"ext-filter": "*",
"fruitcake/php-cors": "^1.3",
"guzzlehttp/guzzle": "^7.8.2",
"guzzlehttp/uri-template": "^1.0",
"illuminate/collections": "^12.0",
"illuminate/macroable": "^12.0",
"illuminate/session": "^12.0",
"illuminate/support": "^12.0",
"symfony/http-foundation": "^7.2.0",
"symfony/http-kernel": "^7.2.0",
"symfony/polyfill-php83": "^1.33",
"symfony/polyfill-php85": "^1.33",
"symfony/mime": "^7.2.0"
},
"autoload": {
"psr-4": {
"Illuminate\\Http\\": ""
}
},
"suggest": {
"ext-gd": "Required to use Illuminate\\Http\\Testing\\FileFactory::image()."
},
"extra": {
"branch-alias": {
"dev-master": "12.x-dev"
}
},
"config": {
"sort-packages": true
},
"minimum-stability": "dev"
}