# API Platform
In order to demonstrate how we can easily change server API while keeping same Admin UI, let's try another more advanced API backend with stateless JWT authentication.
SOURCE CODE
You will find complete source code of this sample in the main repo (opens new window).
# Run backend
First clone the official API Platform demo project (opens new window) and follow readme instructions for running it on docker :
docker-compose up
docker-compose exec php bin/console hautelook:fixtures:load
2
In the end, API Platform you should to access on API Swagger UI at http://localhost:8080 (opens new window).
You can also have a React Admin example running at https://localhost:444 (opens new window), perfect for compare.
This API demo serves mainly 2 entities Book
and Review
.
# Prepare admin UI
First install a new Vue CLI project, as same way as previous tutorial. Then on wizard :
- Select
Hydra
as data provider andJWT
for stateless authentication. - Sets appropriate API endpoint to
http://localhost:8080
. - Let users and material theme disabled.
- We don't need of any profile or impersonation feature either.
Then launch app by yarn serve --open --port 8000
you should arrive to this login page :
# Authentication
Before go further we need to configure axios and plug JWT auth provider to symfony token authentication route :
src/plugins/admin.js
//...
const baseURL = process.env.VUE_APP_API_URL || "http://localhost:8080";
const http = axios.create({
baseURL,
headers: {
Accept: "application/ld+json",
},
});
/**
* Init admin
*/
export default new VuetifyAdmin({
//...
dataProvider: hydraDataProvider(http),
authProvider: jwtAuthProvider(http, {
routes: {
login: "/authentication_token",
},
getToken: (r) => r.token,
}),
//...
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
To resume we add a specific application/ld+json
as accept
header for proper communication with Symfony hydra API. We also add the proper Symfony login route for proper fetching JWT token from valid credentials. getToken
is mainly a response formatter that give us the final token from the HTTP response. It's should be enough to have a functional login process.
Try now to login as [email protected]
/ admin
and you should be redirected at an empty dashboard page.
WARNING
API Platform Demo doesn't provide user information routes, explicit logout, users or profile management, it's up to you to add this features.
# Disable unsupported features
API Platform demo doesn't support by default global search or any configurable items per page for pagination which is forced to 30 items on server side. So let's disable this globally for all VaList
components by default. For that use the options.list
parameter on Vuetify Admin constructor :
//...
export default new VuetifyAdmin({
//...
options: {
dateFormat: "long",
list: {
disableGlobalSearch: true,
disableItemsPerPage: true,
itemsPerPage: 30,
itemsPerPageOptions: [30],
},
},
});
2
3
4
5
6
7
8
9
10
11
12
13
# Add books and reviews
First register new resources :
src/resources/index.js
export default [
{
name: "books",
icon: "mdi-book",
label: "title",
},
{
name: "reviews",
icon: "mdi-comment",
label: "author",
},
];
2
3
4
5
6
7
8
9
10
11
12
Next add new resource links :
src/_nav.js
export default (i18n, admin) => [
{
icon: "mdi-view-dashboard",
text: i18n.t("menu.dashboard"),
link: "/",
},
{ divider: true },
admin.getResourceLink("books"),
admin.getResourceLink("reviews"),
];
2
3
4
5
6
7
8
9
10
Then create all crud pages for both resources by copy paste template code from console browser log as shown on previous tutorial.
# Add supported API features
# Books list
API Platform demo support sorting on all fields for books. We can also filter on both title and author. For that it uses some PHP annotations.
BOOK
api/src/Entity/Book.php
/**
* A book.
*
* //...
*
* @ApiFilter(PropertyFilter::class)
* @ApiFilter(OrderFilter::class, properties={"id", "title", "author", "isbn", "publicationDate"})
*/
class Book
{
//...
/**
* @var string|null The title of the book
*
* @ApiFilter(SearchFilter::class, strategy="ipartial")
* @Assert\NotBlank
* @ORM\Column
* @Groups({"book:read", "review:read"})
* @ApiProperty(iri="http://schema.org/name")
*/
public $title;
//...
/**
* @var string|null The author of this content or rating. Please note that author is special in that HTML 5 provides a special mechanism for indicating authorship via the rel tag. That is equivalent to this and may be used interchangeably
*
* @ApiFilter(SearchFilter::class, strategy="ipartial")
* @Assert\NotBlank
* @ORM\Column
* @Groups("book:read")
* @ApiProperty(iri="http://schema.org/author")
*/
public $author;
//...
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
So let's add them to our books list UI :
src/resources/books/List.js
<template>
<v-card>
<v-card-title>
<h1 class="display-1">
{{ title }}
</h1>
</v-card-title>
<v-card-text>
<va-list :filters="filters">
<va-data-table :fields="fields"></va-data-table>
</va-list>
</v-card-text>
</v-card>
</template>
<script>
export default {
props: ["title"],
data() {
return {
filters: ["title", "author"],
fields: [
{ source: "isbn", sortable: true },
{ source: "title", sortable: true },
{ source: "author", sortable: true },
{ source: "publicationDate", type: "date", sortable: true },
],
};
},
};
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
As the demo give us all reviews associated to books, we can retrieve them on separate columns and format them as material linkable chip group thanks to reference array.
<script>
export default {
props: ["title"],
data() {
return {
filters: ["title", "author"],
fields: [
//...
{
source: "reviews",
type: "reference-array",
attributes: { reference: "reviews", itemText: "id", column: true },
},
],
};
},
};
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
It's enough to render this final page :
Title and author filter will appear under Add filter
dropdown. You can use alwaysOn
if you prefer to keep them always visible on left side.
All review chip will link to the full default review show page.
# Book form
Symfony constraints can be defined on any entity property via the specific Assert
PHP annotation.
ASSERT
api/src/Entity/Book.php
class Book
{
//...
/**
* @var string|null The ISBN of the book
*
* @Assert\Isbn
* @ORM\Column(nullable=true)
* @Groups("book:read")
* @ApiProperty(iri="http://schema.org/isbn")
*/
public $isbn;
/**
* @var string|null The title of the book
*
* @ApiFilter(SearchFilter::class, strategy="ipartial")
* @Assert\NotBlank
* @ORM\Column
* @Groups({"book:read", "review:read"})
* @ApiProperty(iri="http://schema.org/name")
*/
public $title;
/**
* @var string|null A description of the item
*
* @Assert\NotBlank
* @ORM\Column(type="text")
* @Groups("book:read")
* @ApiProperty(iri="http://schema.org/description")
*/
public $description;
/**
* @var string|null The author of this content or rating. Please note that author is special in that HTML 5 provides a special mechanism for indicating authorship via the rel tag. That is equivalent to this and may be used interchangeably
*
* @ApiFilter(SearchFilter::class, strategy="ipartial")
* @Assert\NotBlank
* @ORM\Column
* @Groups("book:read")
* @ApiProperty(iri="http://schema.org/author")
*/
public $author;
/**
* @var \DateTimeInterface|null The date on which the CreativeWork was created or the item was added to a DataFeed
*
* @Assert\Type(\DateTimeInterface::class)
* @Assert\NotNull
* @ORM\Column(type="date")
* @Groups("book:read")
* @ApiProperty(iri="http://schema.org/dateCreated")
*/
public $publicationDate;
//...
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
On admin UI side, here is the final code for a functional book form :
src/resource/books/Form.vue
<template>
<va-form :id="id" :item="item">
<v-row justify="center">
<v-col sm="4">
<v-card>
<v-card-text>
<va-text-input source="isbn"></va-text-input>
<va-text-input source="title"></va-text-input>
<va-text-input source="description" multiline></va-text-input>
<va-text-input source="author"></va-text-input>
<va-date-input source="publicationDate"></va-date-input>
<va-save-button></va-save-button>
</v-card-text>
</v-card>
</v-col>
</v-row>
</va-form>
</template>
<script>
export default {
props: ["id", "item"],
};
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Hydra provider will automatically send data form model to API Platform and return formatted Symfony constraint validations that Vuetify Admin can process :
Server ISBN validation will return error as well if invalid.
Go to hydra provider source code (opens new window) in order to get deeper on how it works. See errors handling section.
# Reviews list
For reviews, demo allows sort by publication date and filtering by book.
FILTER
api/src/Entity/Review.php
/**
* //...
* @ApiFilter(OrderFilter::class, properties={"id", "publicationDate"})
*/
class Review
{
//...
/**
* @var Book The item that is being reviewed/rated
*
* @ApiFilter(SearchFilter::class)
* @Assert\NotNull
* @ORM\ManyToOne(targetEntity=Book::class, inversedBy="reviews")
* @Groups("review:read")
* @ApiProperty(iri="http://schema.org/itemReviewed")
*/
private $book;
//...
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
The optimized reviews list page will be :
src/resources/reviews/List.js
<template>
<v-card>
<v-card-title>
<h1 class="display-1">
{{ title }}
</h1>
</v-card-title>
<v-card-text>
<va-list :filters="filters">
<va-data-table :fields="fields"></va-data-table>
</va-list>
</v-card-text>
</v-card>
</template>
<script>
export default {
props: ["title"],
data() {
return {
filters: [
{
source: "book",
type: "autocomplete",
attributes: { reference: "books", searchQuery: "title" },
},
],
fields: [
{
source: "book",
type: "reference",
attributes: { reference: "books" },
},
"body",
{ source: "rating", type: "rating" },
"author",
{ source: "publicationDate", type: "date", sortable: true },
],
};
},
};
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
We'll use the reference field in order to have a direct link to a given book. The book filter is just a autocomplete input that reference books
resource and use title
as searchQuery.
The result will be :
# Reviews form
Here is the full code :
src/resource/reviews/Form.vue
<template>
<va-form :id="id" :item="item">
<v-row justify="center">
<v-col sm="4">
<v-card>
<v-card-text>
<va-autocomplete-input
source="book"
reference="books"
item-value="@id"
search-query="title"
></va-autocomplete-input>
<va-text-input source="body" multiline></va-text-input>
<va-rating-input source="rating"></va-rating-input>
<va-text-input source="author"></va-text-input>
<va-date-input source="publicationDate"></va-date-input>
<va-save-button></va-save-button>
</v-card-text>
</v-card>
</v-col>
</v-row>
</va-form>
</template>
<script>
export default {
props: ["id", "item"],
};
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
The main particularity is the autocomplete for setting the book to be attached to the review. We need to precise @id
in item-value
prop because API Platform needs a full IRI for get the internal entity reference.
The final result :
# Conclusion
You now have all a basic concepts for integration of Vuetify Admin with real API backend. Go to the next Laravel section which is the most ready to go backend for Vuetify Admin thanks to a powerful composer package that provides all minimal stuff for a real admin with file manager, user and profile management, impersonation and so on. It will also demonstrate the power of included code generators from top to bottom.