Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merge 3.1 #1820

Merged
merged 51 commits into from
Oct 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
c9adccd
fix(file-upload): note about PUT/PATCH higher (#1725)
Nayte91 Feb 22, 2023
a7b3f21
docs: add doctrine odm state processor (#1726)
divine Feb 23, 2023
d4d10bb
Adding a link to the React Admin Symfonycasts video (#1728)
weaverryan Feb 27, 2023
d34dfc6
fix: typos in getting started docs (#1730)
MarionLeHerisson Mar 3, 2023
8174eb9
fix: specify a minimum of 2GB RAM server (#1731)
glukose Mar 8, 2023
f7e7cd4
Update elasticsearch documentation (#1733)
dannyvw Mar 12, 2023
a45e596
fix: update elasticsearch support version (#1732)
dannyvw Mar 14, 2023
8b48937
Merge branch '3.0' into 3.1
alanpoulain Mar 14, 2023
468af85
fix: change AddFormatListener event priority 7 to 28 (#1734)
louismariegaborit Mar 15, 2023
1102a58
fix(elasticsearch): Fix operation signature for RequestBodySearchColl…
dannyvw Mar 23, 2023
6d778c3
Merge 3.0
soyuka Mar 23, 2023
a8a1963
fix: change QueryParameterValidateListener event priority 16 to 2 (#1…
louismariegaborit Mar 28, 2023
8a5f5dd
fix: unnecessary exclamation mark in "if" condition (#1738)
YuriKlimashonak Mar 31, 2023
61daae2
fix: change swagger_definition_name to openapi_definition_name (#1740)
virtualize Apr 12, 2023
7fcd6d7
chore: update extensions.md, add constructor to 8.0 format (#1741)
Xusifob Apr 18, 2023
aad3063
fix: remove obsolete warning (#1743)
alainrinder Apr 18, 2023
53ea5a5
Add deploy on bucket steps
ThomasSamson Apr 21, 2023
721009b
chore: update openapi.md to use PHP 8.0 properties (#1744)
Xusifob Apr 25, 2023
ecb6dce
fix: update identifiers.md (#1745)
Bl00D4NGEL Apr 25, 2023
bbb1fc9
fix: typo in subresources page (#1746)
franckranaivo Apr 25, 2023
6c3ec76
Fix Hoppscotch broken URL (#1747)
edamov Apr 28, 2023
c5d171f
update link to marmelab's documention (#1753)
alexislefebvre May 3, 2023
26aaf47
Add documentation to use Zenstruck Foundry instead of Alice (#1699)
loic425 May 3, 2023
25aefb7
Update bootstrap to v3 (#1640)
kayneth May 3, 2023
40aa45f
Merge pull request #1750 from api-platform/feat/deploy_on_bucket
ThomasSamson May 10, 2023
4076794
Change bucket name
ThomasSamson May 12, 2023
df883f3
Merge pull request #1758 from api-platform/feat/deploy_on_bucket
ThomasSamson May 12, 2023
fd598fc
docs(testing): update testing part using foundry instead of alice (#1…
Romaixn May 24, 2023
77d337b
fix: replace deprecated namespace (#1764)
JuanLuisGarciaBorrego Jun 7, 2023
65ff599
Merge branch '3.1' into merge-3.0
soyuka Jun 7, 2023
88ee77b
Merge pull request #1767 from api-platform/merge-3.0
soyuka Jun 7, 2023
7865d2d
Fix typo in security.md (#1768)
jay-low Jun 8, 2023
cc76d1c
Fix markdown to correctly close preformatted blocks (#1769)
Huppys Jun 9, 2023
1feaacd
Update controllers.md (#1771)
catalin87 Jun 23, 2023
9618dcc
fix: broken data-persisters and data-providers links (#1772)
ginifizz Jun 29, 2023
16a017b
fix: partial pagination parameter updated in 3 (#1778)
xavierleune Jul 11, 2023
9054d45
fix: broken openapi link (#1781)
samnela Jul 18, 2023
155e5e7
fix: typos (#1779)
igneus Jul 18, 2023
24a1352
Docs for the new Docker setup (#1786)
dunglas Jul 19, 2023
fa78818
docs(openapi): Fix broken image link in overriding-the-openapi-specif…
jonnyeom Jul 20, 2023
6fad2d2
Fix images in deployment/docker-compose.md (#1787)
jay-low Jul 25, 2023
97991a8
Better Docker commands (#1789)
dunglas Jul 31, 2023
9a7582b
Improve Docker Compose docs (#1790)
dunglas Jul 31, 2023
05b0eb7
Mention \Stringable identifier support (#1770)
mbrodala Jul 31, 2023
6626010
Add missing function return type in filters.md (#1791)
leonardsimonse Aug 2, 2023
c127a04
fix(distribution): replace dead links (#1793)
Romaixn Aug 10, 2023
ec29ce6
docs(subresources): add yaml examples (#1788)
ttskch Aug 10, 2023
07dd615
doc: replace the old relay url with the new one
Romaixn Aug 11, 2023
0f2f10c
docs(serialization) add missing closing bracket of Attribute (#1800)
jonnyeom Sep 11, 2023
dc1d128
2.7 is EOL
soyuka Oct 16, 2023
0676478
Merge 3.1
soyuka Oct 16, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .github/workflows/cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,14 @@ jobs:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./public
cname: api-platform.com

- name: Auth gcloud
uses: google-github-actions/auth@v1
with:
credentials_json: ${{ secrets.BUCKET_CREDS }}

- name: 'Set up Cloud SDK'
uses: 'google-github-actions/setup-gcloud@v1'

- name: Deploy on bucket
run: gsutil -m rsync -d -r ./public gs://api-platform-website-v3/
2 changes: 1 addition & 1 deletion admin/customizing.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,4 +182,4 @@ For instance, using an autocomplete input is straightforward, [checkout the dedi
API Platform is built on top of [React Admin](https://marmelab.com/react-admin/).
You can use all the features provided by the underlying library with API Platform Admin, including support for [authentication](https://marmelab.com/react-admin/Authentication.html), [authorization](https://marmelab.com/react-admin/Authorization.html) and deeper customization.

To learn more about these capabilities, refer to [the React Admin documentation](https://marmelab.com/react-admin/).
To learn more about these capabilities, refer to [the React Admin documentation](https://marmelab.com/react-admin/documentation.html).
2 changes: 2 additions & 0 deletions admin/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ library to expose a nice, responsive, management interface (Create-Retrieve-Upda

You can **customize everything** by using provided React Admin and [MUI](https://mui.com/) components, or by writing your custom [React](https://reactjs.org/) components.

<p align="center" class="symfonycasts"><a href="https://symfonycasts.com/screencast/api-platform/react-admin?cid=apip"><img src="../distribution/images/symfonycasts-player.png" alt="React Admin Screencast"><br>Watch the React Admin screencast</a></p>

## Features

* Automatically generates an admin interface for all the resources of the API thanks to the hypermedia features of Hydra or to the OpenAPI documentation
Expand Down
521 changes: 292 additions & 229 deletions core/bootstrap.md

Large diffs are not rendered by default.

3 changes: 1 addition & 2 deletions core/controllers.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,6 @@ The entity is retrieved in the `__invoke` method thanks to a dedicated argument

When using `GET`, the `__invoke()` method parameter will receive the identifier and should be called the same as the resource identifier.
So for the path `/user/{uuid}/bookmarks`, you must use `__invoke(string $uuid)`.
**Warning: the `__invoke()` method parameter [MUST be called `$data`](https://symfony.com/doc/current/components/http_kernel.html#4-getting-the-controller-arguments)**, otherwise, it will not be filled correctly!

Services (`$bookPublishingHandler` here) are automatically injected thanks to the autowiring feature. You can type-hint any service
you need and it will be autowired too.
Expand Down Expand Up @@ -503,5 +502,5 @@ book_post_publication:
defaults:
_controller: App\Controller\BookController::createPublication
_api_resource_class: App\Entity\Book
_api_item_operation_name: post_publication
_api_operation_name: post_publication
```
67 changes: 26 additions & 41 deletions core/elasticsearch.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,14 @@ application search, security analytics, metrics, logging, etc.
API Platform comes natively with the **reading** support for Elasticsearch. It uses internally the official PHP client
for Elasticsearch: [Elasticsearch-PHP](https://www.elastic.co/guide/en/elasticsearch/client/php-api/current/index.html).

Be careful, API Platform only supports Elasticsearch >= 6.5.0.
Be careful, API Platform only supports Elasticsearch >= 7.11.0 < 8.0.

## Enabling Reading Support

To enable the reading support for Elasticsearch, simply require the Elasticsearch-PHP package using Composer:

```console
composer require elasticsearch/elasticsearch:^6.0
composer require elasticsearch/elasticsearch:^7.11
```

Then, enable it inside the API Platform configuration:
Expand Down Expand Up @@ -146,10 +146,20 @@ Here is an example of mappings for 2 resources, `User` and `Tweet`, and their mo
// api/src/Model/User.php
namespace App\Model;

use ApiPlatform\Elasticsearch\State\CollectionProvider;
use ApiPlatform\Elasticsearch\State\ItemProvider;
use ApiPlatform\Elasticsearch\State\Options;
use ApiPlatform\Metadata\ApiProperty;
use ApiPlatform\Metadata\ApiResource;

#[ApiResource]
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;

#[ApiResource(
operations: [
new GetCollection(provider: CollectionProvider::class, stateOptions: new Options(index: 'user')),
new Get(provider: ItemProvider::class, stateOptions: new Options(index: 'user')),
],
)]
class User
{
#[ApiProperty(identifier: true)]
Expand All @@ -175,10 +185,20 @@ class User
// api/src/Model/Tweet.php
namespace App\Model;

use ApiPlatform\Elasticsearch\State\CollectionProvider;
use ApiPlatform\Elasticsearch\State\ItemProvider;
use ApiPlatform\Elasticsearch\State\Options;
use ApiPlatform\Metadata\ApiProperty;
use ApiPlatform\Metadata\ApiResource;

#[ApiResource]
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;

#[ApiResource(
operations: [
new GetCollection(provider: CollectionProvider::class, stateOptions: new Options(index: 'tweet')),
new Get(provider: ItemProvider::class, stateOptions: new Options(index: 'tweet')),
],
)]
class Tweet
{
#[ApiProperty(identifier: true)]
Expand All @@ -199,43 +219,8 @@ Keep in mind that it is your responsibility to populate your Elasticsearch index
a custom [state processors](state-processors.md#creating-a-custom-state-processor) or any other mechanism that suits your
project (such as an [ETL](https://en.wikipedia.org/wiki/Extract,_transform,_load)).

To disable elasticsearch index discovery for non-elasticsearch entities you can set `elasticsearch: false` in the `#[ApiResource]` attribute. If this property is absent, all entities will perform an index check during cache warmup to determine if they are on elasticsearch or not.

You're done! The API is now ready to use.

### Creating custom mapping

If you don't follow the Elasticsearch recommendations, you may want a custom mapping between API Platform resources and
Elasticsearch indexes/types.

For example, consider an index being similar to a database in an SQL database and a type being equivalent to a table.
So the `User` and `Tweet` resources of the previous example would become `user` and `tweet` types in an index named `app`:

```yaml
# api/config/packages/api_platform.yaml
parameters:
# ...
env(ELASTICSEARCH_HOST): 'http://localhost:9200'

api_platform:
# ...

mapping:
paths: ['%kernel.project_dir%/src/Model']

elasticsearch:
hosts: ['%env(ELASTICSEARCH_HOST)%']
mapping:
App\Model\User:
index: app
type: user
App\Model\Tweet:
index: app
type: tweet

#...
```

## Filtering

See how to use Elasticsearch filters and how to create Elasticsearch custom filters in [the Filters chapter](filters.md).
Expand Down
6 changes: 3 additions & 3 deletions core/events.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ These built-in event listeners are registered for routes managed by API Platform

Name | Event | [Pre & Post hooks](#custom-event-listeners) | Priority | Description
------------------------------|--------------------|---------------------------------------------|----------|-------------
`AddFormatListener` | `kernel.request` | None | 7 | Guesses the best response format ([content negotiation](content-negotiation.md))
`QueryParameterValidateListener` | `kernel.request` | None | 16 | Validates query parameters
`ReadListener` | `kernel.request` | `PRE_READ`, `POST_READ` | 4 | Retrieves data from the persistence system using the [state providers](data-providers.md) (`GET`, `PUT`, `PATCH`, `DELETE`)
`AddFormatListener` | `kernel.request` | None | 28 | Guesses the best response format ([content negotiation](content-negotiation.md))
`ReadListener` | `kernel.request` | `PRE_READ`, `POST_READ` | 4 | Retrieves data from the persistence system using the [state providers](state-providers.md) (`GET`, `PUT`, `PATCH`, `DELETE`)
`QueryParameterValidateListener` | `kernel.request` | None | 2 | Validates query parameters
`DeserializeListener` | `kernel.request` | `PRE_DESERIALIZE`, `POST_DESERIALIZE` | 2 | Deserializes data into a PHP entity (`POST`); updates the entity retrieved using the state provider (`PUT`, `PATCH`)
`DenyAccessListener` | `kernel.request` | None | 1 | Enforces [access control](security.md) using Security expressions
`ValidateListener` | `kernel.view` | `PRE_VALIDATE`, `POST_VALIDATE` | 64 | [Validates data](validation.md) (`POST`, `PUT`, `PATCH`)
Expand Down
6 changes: 2 additions & 4 deletions core/extensions.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,15 +63,13 @@ use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface;
use ApiPlatform\Metadata\Operation;
use App\Entity\Offer;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\Security\Core\Security;
use Symfony\Bundle\SecurityBundle\Security;

final class CurrentUserExtension implements QueryCollectionExtensionInterface, QueryItemExtensionInterface
{
private $security;

public function __construct(Security $security)
public function __construct(private readonly Security $security)
{
$this->security = $security;
}

public function applyToCollection(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, Operation $operation = null, array $context = []): void
Expand Down
6 changes: 3 additions & 3 deletions core/file-upload.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ recommended you [read the documentation of
VichUploaderBundle](https://github.com/dustin10/VichUploaderBundle/blob/master/docs/index.md)
before proceeding. It will help you get a grasp on how the bundle works, and why we use it.

**Note**: Uploading files won't work in `PUT` or `PATCH` requests, you must use `POST` method to upload files.
See [the related issue on Symfony](https://github.com/symfony/symfony/issues/9226) and [the related bug in PHP](https://bugs.php.net/bug.php?id=55815) talking about this behavior.

## Installing VichUploaderBundle

Install the bundle with the help of Composer:
Expand Down Expand Up @@ -43,9 +46,6 @@ resource (in our case: `Book`).
This example will use a custom controller to receive the file.
The second example will use a custom `multipart/form-data` decoder to deserialize the resource instead.

**Note**: Uploading files won't work in `PUT` or `PATCH` requests, you must use `POST` method to upload files.
See [the related issue on Symfony](https://github.com/symfony/symfony/issues/9226) and [the related bug in PHP](https://bugs.php.net/bug.php?id=55815) talking about this behavior.

### Configuring the Resource Receiving the Uploaded File

The `MediaObject` resource is implemented like this:
Expand Down
42 changes: 22 additions & 20 deletions core/filters.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Filters

API Platform provides a generic system to apply filters and sort criteria on collections.
Useful filters for Doctrine ORM, MongoDB ODM and ElasticSearch are provided with the library.
Useful filters for Doctrine ORM, MongoDB ODM, and ElasticSearch are provided with the library.

You can also create custom filters that fit your specific needs.
You can also add filtering support to your custom [state providers](state-providers.md) by implementing interfaces provided
Expand All @@ -23,7 +23,7 @@ to a Resource in two ways:

1. Through the resource declaration, as the `filters` attribute.

For example having a filter service declaration in `services.yaml`:
For example, having a filter service declaration in `services.yaml`:

```yaml
# api/config/services.yaml
Expand Down Expand Up @@ -254,15 +254,15 @@ The above URLs will return all offers for the product having the following IRI a

### Date Filter

The date filter allows to filter a collection by date intervals.
The date filter allows filtering a collection by date intervals.

Syntax: `?property[<after|before|strictly_after|strictly_before>]=value`

The value can take any date format supported by the [`\DateTime` constructor](https://www.php.net/manual/en/datetime.construct.php).

The `after` and `before` filters will filter including the value whereas `strictly_after` and `strictly_before` will filter excluding the value.

Like others filters, the date filter must be explicitly enabled:
Like other filters, the date filter must be explicitly enabled:

[codeSelector]

Expand Down Expand Up @@ -533,7 +533,7 @@ You can filter offers by joining two values, for example: `/offers?price[gt]=12.

### Exists Filter

The exists filter allows you to select items based on a nullable field value.
The "exists" filter allows you to select items based on a nullable field value.
It will also check the emptiness of a collection association.

Syntax: `?exists[property]=<true|false|1|0>`
Expand Down Expand Up @@ -582,7 +582,7 @@ App\Entity\Offer:

[/codeSelector]

Given that the collection endpoint is `/offers`, you can filter offers on nullable field with the following query: `/offers?exists[transportFees]=true`.
Given that the collection endpoint is `/offers`, you can filter offers on the nullable field with the following query: `/offers?exists[transportFees]=true`.

It will return all offers where `transportFees` is not `null`.

Expand All @@ -600,7 +600,7 @@ api_platform:

### Order Filter (Sorting)

The order filter allows to sort a collection against the given properties.
The order filter allows sorting a collection against the given properties.

Syntax: `?order[property]=<asc|desc>`

Expand Down Expand Up @@ -990,8 +990,8 @@ api_platform:

### Match Filter

The match filter allows to find resources that [match](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-match-query.html)
the specified text on full text fields.
The match filter allows us to find resources that [match](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-match-query.html)
the specified text on full-text fields.

Syntax: `?property[]=value`

Expand Down Expand Up @@ -1021,7 +1021,7 @@ Given that the collection endpoint is `/tweets`, you can filter tweets by messag

### Term Filter

The term filter allows to find resources that contain the exact specified
The term filter allows us to find resources that contain the exact specified
[terms](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-term-query.html).

Syntax: `?property[]=value`
Expand Down Expand Up @@ -1194,7 +1194,7 @@ final class RegexpFilter extends AbstractFilter
{
protected function filterProperty(string $property, $value, QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, Operation $operation = null, array $context = []): void
{
// otherwise filter is applied to order and page as well
// Otherwise filter is applied to order and page as well
if (
!$this->isPropertyEnabled($property, $resourceClass) ||
!$this->isPropertyMapped($property, $resourceClass)
Expand Down Expand Up @@ -1260,7 +1260,7 @@ class Offer
You can now use this filter in the URL like `http://example.com/offers?regexp_email=^[FOO]`. This new filter will also
appear in OpenAPI and Hydra documentations.

In the previous example, the filter can be applied on any property. You can also apply this filter on a specific property:
In the previous example, the filter can be applied to any property. You can also apply this filter on a specific property:

```php
<?php
Expand Down Expand Up @@ -1303,10 +1303,11 @@ class Offer
```
These properties can then be accessed in the custom filter like this:
```php
//App/Filter/CustomAndFilter.php
protected function filterProperty(string $property, $value, QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, Operation $operation = null, array $context = []) {
// api/src/Filter/CustomAndFilter.php

protected function filterProperty(string $property, $value, QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, Operation $operation = null, array $context = []): void {
$rootAlias = $queryBuilder->getRootAliases()[0];
foreach (array_keys($this->getProperties()) as $prop) { //NOTE: we use array_keys because getProperties() returns a map of property => strategy
foreach(array_keys($this->getProperties()) as $prop) { // we use array_keys() because getProperties() returns a map of property => strategy
if (!$this->isPropertyEnabled($prop, $resourceClass) || !$this->isPropertyMapped($prop, $resourceClass)) {
return;
}
Expand Down Expand Up @@ -1335,7 +1336,7 @@ services:
tags: [ 'api_platform.filter' ]
```

In the previous example, the filter can be applied on any property. However, thanks to the `AbstractFilter` class,
In the previous example, the filter can be applied to any property. However, thanks to the `AbstractFilter` class,
it can also be enabled for some properties:

```yaml
Expand Down Expand Up @@ -1394,10 +1395,11 @@ Suppose you want to use the [match filter](#match-filter) on a property named `$
namespace App\ElasticSearch;

use ApiPlatform\Elasticsearch\Extension\RequestBodySearchCollectionExtensionInterface;
use ApiPlatform\Metadata\Operation;

class AndOperatorFilterExtension implements RequestBodySearchCollectionExtensionInterface
{
public function applyToCollection(array $requestBody, string $resourceClass, ?string $operationName = null, array $context = []): array
public function applyToCollection(array $requestBody, string $resourceClass, ?Operation $operation = null, array $context = []): array;
{
$requestBody['query'] = $requestBody['query'] ?? [];
$andQuery = [
Expand All @@ -1415,7 +1417,7 @@ class AndOperatorFilterExtension implements RequestBodySearchCollectionExtension
### Using Doctrine ORM Filters

Doctrine ORM features [a filter system](http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/filters.html) that allows the developer to add SQL to the conditional clauses of queries, regardless of the place where the SQL is generated (e.g. from a DQL query, or by loading associated entities).
These are applied on collections and items and therefore are incredibly useful.
These are applied to collections and items and therefore are incredibly useful.

The following information, specific to Doctrine filters in Symfony, is based upon [a great article posted on Michaël Perrin's blog](http://blog.michaelperrin.fr/2014/12/05/doctrine-filters/).

Expand Down Expand Up @@ -1594,7 +1596,7 @@ On the first property, `name`, it's straightforward. The first attribute argumen
#[ApiFilter(SearchFilter::class, strategy: 'partial')]
```

In the second attribute, we specify `properties` on which the filter should apply. It's necessary here because we don't want to filter `colors` but the `prop` property of the `colors` association.
In the second attribute, we specify `properties` to which the filter should apply. It's necessary here because we don't want to filter `colors` but the `prop` property of the `colors` association.
Note that for each given property we specify the strategy:

```php
Expand Down Expand Up @@ -1632,7 +1634,7 @@ class DummyCar

```

The `BooleanFilter` is applied to every `Boolean` property of the class. Indeed, in each core filter we check the Doctrine type. It's written only by using the filter class:
The `BooleanFilter` is applied to every `Boolean` property of the class. Indeed, in each core filter, we check the Doctrine type. It's written only by using the filter class:

```php
#[ApiFilter(BooleanFilter::class)]
Expand Down
Loading