Sign up with your email address to be the first to know about new products, VIP offers, blog features & more.

Instalando Apigility, Doctrine e OAuth2 – FASTEST ROUTE

Instalação do Apigility

Instalar via Composer

O caminho path/to/install refere-se ao diretório onde o apigility será instalado.

$ composer create-project zfcampus/zf-apigility-skeleton path/to/install

Criar Virtual Host no Apache

Modifique de acordo com o seu ambiente de desenvolvimento:

<VirtualHost apigility.local:80>
    <Directory "path/to/install/public">
        Options FollowSymLinks Indexes
        AllowOverride All
        Order deny,allow
        allow from All
    </Directory>
    ServerName apigility.local
    ServerAlias apigility.local
    DocumentRoot "path/to/install/public"
    ErrorLog "path/to/apache/logs/apigility.local.err"
    CustomLog "path/to/apache/logs/apigility.local.log" combined
</VirtualHost>

Habilitar Ambiente de Desenvolvimento

Execute o comando do Composer para habilitar o ambiente de desenvolvimento:

$ composer development-enable

Criar a Primeira API

Acesse o Apigility no navegador (conforme configuração do hosts feita acima, ex.: http://apigility.local/).

01

Clique no botão New API no menu à esquerda, crie um nome para sua API e clique em Create.

Atenção: O nome da API será usado para criar o namespace do módulo no Zend Framework, então deve-se seguir a convenção de se utilizar PascalCase.

02

Instalação do Doctrine

Instalar Plugin e Módulo

Execute o comando abaixo para instalar o plugin do Doctrine para Apigility e o DoctrineORMModule para o Zend Framework:

$ composer require zfcampus/zf-apigility-doctrine && composer require doctrine/doctrine-orm-module

Durante a instalação, será solicitado escolher em qual arquivo serão armazenadas as configurações dos módulos instalados. Responda conforme os valores informados abaixo:

Please select which config file you wish to inject 'Zend\Form' into:
  [0] Do not inject
  [1] config/modules.config.php
  [2] config/development.config.php.dist
  Make your selection (default is 0): 1 # Informe 1 e tecle ENTER

Remember this option for other packages of the same type? (y/N) y # Informe y e tecle ENTER

Please select which config file you wish to inject 'DoctrineModule' into:
  [0] Do not inject
  [1] config/modules.config.php
  [2] config/development.config.php.dist
  Make your selection (default is 0): 1 # Informe 1 e tecle ENTER

# Após concluir a instalação do plugin do Doctrine para Apigility, será iniciada a instalação do DoctrineORMModule

Please select which config file you wish to inject 'DoctrineORMModule' into:
  [0] Do not inject
  [1] config/modules.config.php
  [2] config/development.config.php.dist
  Make your selection (default is 0): 1 # Informe 1 e tecle ENTER

Remember this option for other packages of the same type? (y/N) y # Informe y e tecle ENTER

Ajustar Instalação

Após a instalação do plugin do Doctrine e do DoctrineORMModule, será necessário fazer um ajuste nos arquivos de configuração, conforme abaixo:

// config/modules.config.php

<?php
return [
    // ...

    'DoctrineModule',
    'Phpro\DoctrineHydrationModule',
    'ZF\Apigility\Doctrine\Admin', // Remova esta linha
    'ZF\Apigility\Doctrine\Server',
    'DoctrineORMModule',
    'Application',

    // ...
];

// config/development.config.php

<?php
return [
    // Development time modules
    'modules' => [
        'ZendDeveloperTools',
        'ZF\Apigility\Admin',
        'ZF\Apigility\Doctrine\Admin', // Insira esta linha aqui
    ],
    
    // ...
];

 

ATENÇÃO: a ordem é importante, uma vez que há dependência entre os módulos. Portanto, o módulo Doctrine\Admin deve ser carregado após o módulo Admin. Além disso, no arquivo modules.config.php, não mude a ordem os módulo que lá estão, uma vez que também há dependência entre eles.

Configurar Conexão com o Banco

Crie um arquivo de configuração local do Doctrine:

// config/autoload/doctrine.local.php

<?php
return [
    'doctrine' => [
        'connection' => [
            'orm_default' => [
                'driverClass' => 'Doctrine\DBAL\Driver\PDOMySql\Driver',
                'params' => [
                    'host'     => 'localhost',
                    'port'     => '3306',
                    'user'     => 'root',
                    'password' => '',
                    'dbname'   => 'apigility',
                    'charset' => 'utf8',
                    'driverOptions' => ['SET NAMES utf8']
                ]
            ]
        ]
    ]
];

 

Configurar Doctrine no Módulo da API

No arquivo module.config.php, insira o seguinte elemento no array de retorno:

Substitua Super pelo nome da sua API

// module/Super/config/module.config.php

<?php
 return [
     'doctrine' => [
         'driver' => [
             'Super_driver' => [
                 'class' => \Doctrine\ORM\Mapping\Driver\AnnotationDriver::class,
                 'cache' => 'array',
                 'paths' => [
                     './module/Super/src/V1/Entity',
                 ],
             ],
             'orm_default' => [
                 'drivers' => [
                     'Super' => 'Super_driver',
                 ],
             ],
         ],
     ],

     // ...
];

Verificar Instalação

Acesse o Apigility no navegador e verifique se o Doctrine está corretamente instalado:

03

Se houver algum erro, por favor verifique se você está usando uma versão maior ou igual ao PHP 7.1. Enfrentei o seguinte problema, porque esta usando o PHP 7.0:

PHP Fatal error:  Uncaught TypeError: Return value of Doctrine\Common\Annotations\AnnotationRegistry::registerLoader() must be an instance of Doctrine\Common\Annotations\void, none returned in /Applications/MAMP/htdocs/apigility/vendor/doctrine/annotations/lib/Doctrine/Common/Annotations/AnnotationRegistry.php:117
Stack trace:
#0 /Applications/MAMP/htdocs/apigility/vendor/doctrine/doctrine-module/src/DoctrineModule/Module.php(57): Doctrine\Common\Annotations\AnnotationRegistry::registerLoader(Object(Closure))
#1 /Applications/MAMP/htdocs/apigility/vendor/zendframework/zend-modulemanager/src/Listener/InitTrigger.php(33): DoctrineModule\Module->init(Object(Zend\ModuleManager\ModuleManager))
#2 /Applications/MAMP/htdocs/apigility/vendor/zendframework/zend-eventmanager/src/EventManager.php(271): Zend\ModuleManager\Listener\InitTrigger->__invoke(Object(Zend\ModuleManager\ModuleEvent))
#3 /Applications/MAMP/htdocs/apigility/vendor/zendframework/zend-eventmanager/src/EventManager.php(143): Zend\EventManager\EventManager->triggerListe in /Applications/MAMP/htdocs/apigility/vendor/doctrine/annotations/lib/Doctrine/Common/Annotations/AnnotationRegistry.php on line 117

 

Este erro ocorre porque a versão mais recente do Doctrine\Common\Annotations usa o PHP 7.1. Essa é a razão dele usar void como um tipo de retorno, e isso não é suportado no PHP 7.0.

 

Instalação do Plugin para CORS

Instalar o Plugin

Execute o comando abaixo para instalar o plugin de configuração do CORS:

$ composer require zfr/zfr-cors:1.*

Novamente será solicitado escolher em qual arquivo serão armazenadas as configurações. Informe os valores conforme abaixo:

Please select which config file you wish to inject 'ZfrCors' into:
  [0] Do not inject
  [1] config/modules.config.php
  [2] config/development.config.php.dist
  Make your selection (default is 0): 1 # Informe 1 e tecle ENTER

Remember this option for other packages of the same type? (y/N) y # Informe y e tecle ENTER

Configurar o CORS

Copie o arquivo de exemplo de configuração do diretório de instalação do plugin para o diretório dos arquivos de autoload e renomeie-o removendo a extensão .dist. O comando abaixo fará tudo isso:

$ cp ./vendor/zfr/zfr-cors/config/zfr_cors.global.php.dist ./config/autoload/zfr_cors.global.php

Modifique o arquivo de configuração do CORS conforme o exemplo abaixo, descomentando todos os parâmetros de configuração no array (os comentários originais foram removidos para melhor visualização):

// config/autoload/zfr_cors.global.php

<?php
return [
    'zfr_cors' => [
        // Insira aqui os domínios que poderão fazer requisições à API e os métodos que poderão ser utilizados
        'allowed_origins' => ['http://example.com'],
        'allowed_methods' => ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
        // Insira os cabeçalhos permitidos nas requisições conforme abaixo
        'allowed_headers' => ['Authorization', 'Content-Type', 'Access-Control-Allow-Origin'],
        // Outras configurações
        'max_age' => 120,
        'exposed_headers' => [],
        'allowed_credentials' => false,
    ],
];

Habilitar Extensões do Chrome

Algumas requisições à API (como ao endpoint /oauth que será criado a seguir) não são compatíveis com plugins do chrome como Postman ou ARC e, ao tentar fazer o request, o seguinte erro será exibido na tela:

Fatal error: Uncaught Zend\Uri\Exception\InvalidArgumentException: no class registered for scheme “chrome-extension” in C:\ampps\www\apigility\vendor\zendframework\zend-uri\src\UriFactory.php on line 104

Para corrigir isso, altere o arquivo Module.php conforme abaixo:

// module/Super/Module.php

<?php

//...

use Zend\Uri\UriFactory; // Insira esta linha

class Module implements ApigilityProviderInterface
{
    // ...
    
    // Insira o método abaixo
    public function onBootstrap()
    {
        UriFactory::registerScheme('chrome-extension', 'Zend\Uri\Uri');
    }
}

Após fazer as alterações acima, será possível fazer requisições através dos plugins do Chrome como Postman e ARC. Porém, como as requisições feitas por esses plugins partem de uma origem diferente da sua aplicação, a API retornará o seguinte erro:

# Erro retornado no Postman
Status: 403 The origin "chrome-extension://fhbjgbiflinjbdggehcddcbncdddomop" is not authorized

O erro acima ocorre porque o Postman não foi adicionado à lista de origens permitidas para CORS no Apigility. Para corrigir isso, basta inserir a origem no array de configuração do CORS:

// config/autoload/zfr_cors.global.php

<?php
return [
    'zfr_cors' => [
        // Insira o endereço de origem no array abaixo
        'allowed_origins' => ['http://example.com', 'chrome-extension://fhbjgbiflinjbdggehcddcbncdddomop'],
        // ...
    ];
];

Nota: ao criar uma aplicação cliente, é necessário adicionar sua url na lista de origens permitidas. A exemplo, se uma aplicação cliente estiver no endereço frontend.local e a api estiver no endereço backend.local, as requisições não serão aceitas a não ser que se adicione o endereço frontend.local à lista de origens permitidas.

Configuração do OAuth2

Criar Tabelas no Banco

Importe o arquivo vendor/zfcampus/zf-oauth2/data/db_oauth2.sql para dentro do banco de dados. As seguintes tabelas serão criadas:

oauth_access_tokens
oauth_authorization_codes
oauth_clients
oauth_jwt
oauth_refresh_tokens
oauth_scopes
oauth_users

Essas tabelas serão utilizadas pelo Apigility para fazer a autenticação e autorização dos usuários e seus respectivos clientes durante o acesso à API.

Nota: É importante notar que o Apigility usará a tabela oauth_users para obter os dados dos usuários, como login e senha. Leve em conta que configurar o Apigility para utilizar outra tabela que não seja a padrão (oauth_users) pode-se tornar cansativo e gerar muitos erros inesperados. Portanto, caso o banco de dados já tenha uma tabela previamente criada para armazenamento dos usuários, recomenda-se ajustar o modelo para passar a utilizar a tabela oauth_users para esse fim.

Criar Adapter

Acesse o Apigility no navegador e crie um Authentication Adapter para o OAuth2:

04

Preencha os campos conforme o exemplo abaixo e clique em Save:

05

Cadastrar Clientes da API

A fim de se fazer as requisições, o OAuth2 precisará receber os dados de acesso do cliente que estará fazendo a solicitação. Para tanto, é necessário que seja feito o cadastro dos clientes que usarão a API diretamente no Banco de Dados.

Cadastre um novo cliente na tabela oauth_clients conforme exemplo abaixo:

-- Cria um novo cliente com os dados abaixo
-- client_id: testclient
-- client_secret: testpass

INSERT INTO oauth_clients (client_id, client_secret, redirect_uri)
VALUES ('testclient', '$2y$10$5ICo6mbnWLsptjCZVfMu1e7p04FYpgiZydEG1KD4MI8Q2fcwuCu8e', '/oauth/receivecode');

O exemplo acima criará um cliente chamado testclient com a senha testpass. O campo client_secret armazena senhas com hash utilizando bcrypt. Para gerar uma senha, utilize o seguinte comando:

# Gera hash da senha 123456
php vendor/zfcampus/zf-oauth2/bin/bcrypt.php 123456

# Output
$2y$10$FnkngtFrGeu1DPtasu68euDpJksCU3o092gIFV0H.N0WW2YTB88.K

Clientes com senha são chamados de clientes confidenciais (mais detalhes). Caso a API venha a oferecer acesso a clientes públicos, basta inserir o registro do novo cliente no banco sem inserir um client_secret. O Apigility automaticamente tratará a requisição e idenficará que se trata de um cliente público e não confidencial.

Cadastrar Usuários

O cadastro de usuários será feito na aplicação. Porém, para finalidade de teste, pode-se fazer a inserção de um novo usuário diretamente no banco de dados:

-- Cria um novo usuário com os dados abaixo
-- username: johndoe
-- password: 123456

INSERT INTO oauth_users (username, password, first_name, last_name)
VALUES ('johndoe', '$2y$10$FnkngtFrGeu1DPtasu68euDpJksCU3o092gIFV0H.N0WW2YTB88.K', 'John', 'Doe');

Testar Autenticação

Atenção: O exemplo a seguir testará se a API está configurada corretamente para receber as requisições no endpoint /oauth e gerenciar o login utilizando OAuth2.

Tenha em mente que esse é apenas um teste para verificar se tudo está configurado corretamente, e que o método apresentado abaixo não deve ser utilizado em produção, uma vez que ele faz a autenticação passando o client_secret no corpo da requisição.

Para saber mais sobre como implementar os mecanismos de autenticação utilizando OAuth2 em diferentes contextos, acesse a documentação oficial.

Para testar se a autenticação está funcionando corretamente, faça a seguinte requisição ao endereço http://<apigility URL>/oauth utilizando o método POST:

POST /oauth HTTP/1.1
Accept: application/json
Content-Type: application/json

{
    "username": "johndoe",
    "password": "123456",
    "grant_type": "password",
    "client_id": "testclient",
    "client_secret": "testpass"
}

A requisição acima deverá retornar o token de acesso Bearer:

{
  "access_token": "403000e75765e89e7566472c945e265734f238b4",
  "expires_in": 3600,
  "token_type": "Bearer",
  "scope": null,
  "refresh_token": "34e9f4b1e5730424bf89cffdf69fd5d45693ea7d"
}

Criação dos Serviços

Gerar Entidades com Doctrine

Antes de se criar quaisquer serviços, é preciso primeiro gerar as entidades utilizando o CLI do Doctrine e, para isso, é necessário fazer um ajuste na tabela oauth_scopes. Caso contrário, o seguinte erro será exibido:

[Doctrine\ORM\Mapping\MappingException]
  Table oauth_scopes has no primary key. Doctrine does not support reverse en
  gineering from tables that don't have a primary key.

Isso acontece pois o Doctrine não consegue lidar com tabelas que não possuam uma chave primária (apesar de que tabelas de ligação com chaves compostas funcionam sem problemas). Para corrigir o problema, execute o comando abaixo no banco de dados:

-- Insere a coluna id no início da tabela
ALTER TABLE `oauth_scopes`
ADD COLUMN `id` INT NOT NULL AUTO_INCREMENT FIRST,
ADD PRIMARY KEY (`id`);

Para gerar as entidades utilizando o CLI do Doctrine, utilize o comando abaixo substituindo o nome do módulo conforme necessário:

$ vendor/bin/doctrine-module orm:convert-mapping --force --from-database --namespace="Super\\V1\\Entity\\" annotation ./module/Super/src/

Nota: para gerar apenas uma entidade ao invés de todas, utilize o flag --filter "Entidade".

O Doctrine criará os arquivos das entidades no diretório incorreto. Para corrigir isso, execute o comando abaixo, alterando o nome do módulo conforme necessário:

$ mv module/Super/src/Super/V1/Entity module/Super/src/V1/Entity && rm -rf module/Super/src/Super

O próximo passo é gerar os getters e setters das entidades. Para fazer isso, execute o comando abaixo:

$ vendor/bin/doctrine-module orm:generate-entities module/Super/src --generate-annotations=true

Nota: Novamente, caso deseje gerar os getters e setters para apenas uma entidade, utilize a flag --filter "Entidade".

Mais uma vez o Doctrine criará os arquivos no diretório incorreto. Para resolver isso, execute o seguinte comando:

$ rm -rf module/Super/src/V1/Entity && mv module/Super/src/Super/V1/Entity module/Super/src/V1/Entity && rm -rf module/Super/src/Super

Atenção: Caso não precise utilizar as tabelas geradas pelo OAuth2 no banco de dados, é recomendável excluir os arquivos das entidades que foram gerados automaticamente pelo Doctrine. Em suma, exclua todos os arquivos cujo nome comece com OAuth, como por exemplo OauthAccessTokens.php e OauthScopes.php.

Vale observar que no início, ao configurar o banco, recomendou-se armazenar os usuários na tabela oauth_users. Portanto, ao excluir os arquivos das entidades não utilizadas, lembre-se de não excluir o arquivo OauthUsers.php, uma vez que ele será utilizado em sua aplicação para manipular os usuários no banco de dados.

Recomenda-se ainda renomear o arquivo e a classe de OauthUsers.php para User.php a fim de simplificar o nome da classe para facilitar seu uso.

Criar os Serviços

A última etapa é criar os serviços conectados com Doctrine. Para fazer isso, acesse o Apigility no navegador e crie um novo serviço conforme as imagens abaixo:

06

07

Verifique se os serviços foram criados corretamente clicando no nome do novo serviço no menu à esquerda:

08

Testar os Serviços

Para testar os serviços, basta fazer uma requisição ao seu endpoint. O exemplo abaixo é um retorno à requisição GET no endpoint http://<apigility URL>/product:

{
  "_links": {
    "self": {
      "href": "http://apigility.local/product"
    }
  },
  "_embedded": {
    "product": []
  },
  "page_count": 0,
  "page_size": 25,
  "total_items": 0,
  "page": 0
}

IMPORTANTE: Ao gerar as entidades automaticamente, o Doctrine nomeia a variável que mapeia para a chave primária da tabela conforme o nome da coluna, converte-o para camelCase. Ex.: se uma tabala possui como PK a coluna cod_produto, o Doctrine criará a variável $codProduto para mapear essa propriedade para a coluna em questão. Essa variável é chamada de Entity Identifier.

Quando se cria um serviço conectado ao Doctrine, o nome da variável utilizada como identificador da entidade é id por padrão. Isso ocorre independentemente de como a entidade está configurada. Portanto, no exemplo acima em que o identificador da entidade se chama $codProduto, ao se tentar fazer uma requisição passando a ID do produto = 4, o Apigility solicitará ao Doctrine para encontrar a entidade cujo id seja igual a 4, retornando assim um erro. O erro ocorrerá pois o Doctrine não encontrará a propriedade id na entidade (ele esperaria receber o valor para a variável codProduto e não id).

Para resolver esse problema, há dois caminhos: o primeiro (e mais fácil) é alterar o nome da variável na entidade simplesmente para $id. Assim, não é preciso fazer nenhuma mudança nas configurações do Apigility.

A segunda opção é alterar o valor do campo Entity identifier name nas configurações do serviço (em General Settings) no painel do Apigility no navegador. O problema é que existe um bug que ignora essa mudança, mesmo após seu salvamento (ele não persiste o valor no arquivo, como seria esperado). Portanto, para contornar isso, o correto é ir diretamente no arquivo module/<Namespace>gt;/src/config/module.config.php e fazer as devidas modificações manualmente.

Bloquear Acesso a Rotas

É possível restringir uma rota para que apenas usuários autenticados e autorizados possam acessá-la. Para isso, clique no serviço desejado no menu à esquerda e em seguida selecione a aba Authorization. Escolha os métodos HTTP que deverão ser bloqueados. Apenas como exemplo, vamos bloquear o acesso ao endpoint /product com o verbo GET para uma entidade.

Nota: somente as rotas habilitadas nas configurações do serviço estarão disponíveis. Para habilitar/desabilitar rotas para o serviço, acesse a aba General Settings e marque ou desmarque as caixas de seleção em HTTP Entity Methods e HTTP Collection Methods.

10

Para testar se uma rota está restrita apenas a usuários autorizados, cadastre um registro na tabela product e, em seguida, faça uma requisição para /product/[id], e o seguinte retorno será esperado:

{
  "type": "http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html",
  "title": "Forbidden",
  "status": 403,
  "detail": "Forbidden"
}

Isso significa que a rota está restrita. Para poder acessá-la, solicite um token de acesso conforme [explicado acima](#Testar Autenticação).
Ao obter o token, basta fazer uma requisição à rota passando o código Bearer como valor do parâmetro Authorization no cabeçalho da requisição, desta forma:

GET /product/1 HTTP/1.1
Host: apigility.local
Authorization: Bearer 403000e75765e89e7566472c945e265734f238b4

O seguinte resultado será esperado:

{
  "id": 1,
  "name": "My product",
  "price": "5.00",
  "inStock": true,
  "_links": {
    "self": {
      "href": "http://apigility.local/product/1"
    }
  }
}

Deploy para Produção

Gerar o Package

Para gerar o package para produção, basta acessar o Apigility no navegador e ajuster as configurações do pacote na aba Package conforme a imagem abaixo:

09

O arquivo final será gerado e o download será iniciado.

4 Responses
  • jhonatas felipe
    November 6, 2018

    Tudo bom ! estou tentando fazer colocar o apigility em produção mas sempre que subo ele para o servido ele me mostra um erro na hora de fazer requisições, mas a documentação está funcionarndo normal

  • JEFERSON
    April 24, 2019

    Estou com dificuldade para instalar o doctrine.

    Problem 1
    – Installation request for zfcampus/zf-apigility-doctrine ^2.3 -> satisfiable by zfcampus/zf-apigility-doctrine[2.3.0].
    – Conclusion: remove zendframework/zend-hydrator 3.0.2
    – Conclusion: don’t install zendframework/zend-hydrator 3.0.2
    – zfcampus/zf-apigility-doctrine 2.3.0 requires phpro/zf-doctrine-hydration-module ^2.0.1 || ^3.0 || ^4.1 -> satisfiable by phpro/zf-doctrine-hydration-module[v2.0.1, v3.0.0, v4.1.0].
    – phpro/zf-doctrine-hydration-module v3.0.0 requires zendframework/zend-hydrator ^1.1 || ^2.2.1 -> satisfiable by zendframework/zend-hydrator[1.1.0, 2.2.1, 2.2.2, 2.2.3, 2.3.0, 2.3.1, 2.4.0, 2.4.1].
    – phpro/zf-doctrine-hydration-module v4.1.0 requires zendframework/zend-hydrator ^1.1 || ^2.2.1 -> satisfiable by zendframework/zend-hydrator[1.1.0, 2.2.1, 2.2.2, 2.2.3, 2.3.0, 2.3.1, 2.4.0, 2.4.1].
    – phpro/zf-doctrine-hydration-module v2.0.1 requires zendframework/zend-hydrator ~1.0 || ^2.0 -> satisfiable by zendframework/zend-hydrator[1.0.0, 1.1.0, 2.0.0, 2.1.0, 2.2.0, 2.2.1, 2.2.2, 2.2.3, 2.3.0, 2.3.1, 2.4.0, 2.4.1].
    – Can only install one of: zendframework/zend-hydrator[1.1.0, 3.0.2].
    – Can only install one of: zendframework/zend-hydrator[1.0.0, 3.0.2].
    – Can only install one of: zendframework/zend-hydrator[2.1.0, 3.0.2].
    – Can only install one of: zendframework/zend-hydrator[2.2.0, 3.0.2].
    – Can only install one of: zendframework/zend-hydrator[2.2.1, 3.0.2].
    – Can only install one of: zendframework/zend-hydrator[2.2.2, 3.0.2].
    – Can only install one of: zendframework/zend-hydrator[2.2.3, 3.0.2].
    – Can only install one of: zendframework/zend-hydrator[2.3.0, 3.0.2].
    – Can only install one of: zendframework/zend-hydrator[2.3.1, 3.0.2].
    – Can only install one of: zendframework/zend-hydrator[2.4.0, 3.0.2].
    – Can only install one of: zendframework/zend-hydrator[2.4.1, 3.0.2].
    – Can only install one of: zendframework/zend-hydrator[2.0.0, 3.0.2].
    – Installation request for zendframework/zend-hydrator (locked at 3.0.2) -> satisfiable by zendframework/zend-hydrator[3.0.2].

    Installation failed, reverting ./composer.json to its original content.

  • Harry
    July 2, 2019

    Hi, thanks for this amazing tutorial! Best on the web tbh (even though with the Google Translate :D) – Just one thing is not clear to me. At what step did your Doctrine generate the Product entity? The one that is listed when you are creating the service. Mine just shows the Oauth entities.

  • тонировка дубай
    September 15, 2021

    Just desire to sayy your article iss as amazing. The clearness in your
    post is just grdeat and i ccan assume you’re an expert on this subject.
    Fine with your permission let me to grab your
    RSS feed to keep updated with forthcoming post.
    Thanks a millikn and please continue the rewarding work.
    тонировка дубай
    Ekskurzia Dubai
    екскурзия дубай

What do you think?

Your email address will not be published. Required fields are marked *