O framework Laravel se tornou um recurso essencial para desenvolvedores que criam serviços web. Como uma ferramenta de código aberto, ele oferece uma infinidade de funcionalidades prontas para uso que permitem criar aplicativos robustos e funcionais.

Uma delas é o Laravel Scout, uma biblioteca para gerenciar os índices de pesquisa do seu aplicativo. Sua flexibilidade permite configurações detalhadas, bem como selecionar entre os drivers Algolia, Meilisearch, MySQL ou Postgres para armazenar os índices.

Aqui exploraremos essa ferramenta em profundidade, ensinando você a adicionar ao seu aplicativo Laravel o suporte à pesquisa de texto completo por meio do driver. Você modelará um aplicativo Laravel de demonstração para armazenar nomes de maquetes de trens, e então usará o Laravel Scout para adicionar uma pesquisa ao aplicativo.

Pré-requisitos

Para acompanhar, você deve ter:

  • O compilador PHP instalado em seu computador. Este tutorial usa a versão 8.1 do PHP
  • O mecanismo Docker ou o Docker Desktop instalado em seu computador
  • Uma conta de nuvem Algolia, que você pode criar gratuitamente

Como instalar o Scout em um projeto Laravel

Para usar o Scout, você deve primeiro criar o aplicativo Laravel ao qual pretende adicionar a funcionalidade de pesquisa. O script Laravel-Scout Bash contém os comandos para gerar um aplicativo Laravel em um container Docker. Usar o Docker significa que você não precisa instalar software de suporte adicional, como um banco de dados MySQL.

O script Laravel-Scout usa a linguagem de script Bash, portanto, você deve executá-lo em um ambiente Linux. Se estiver no Windows, certifique-se de configurar o Windows Subsystem for Linux (WSL).

Para isso, execute o seguinte comando em seu terminal para definir a distribuição Linux de sua preferência.

wsl -s ubuntu

Em seguida, navegue até o local em seu computador onde você gostaria de colocar o projeto. O script Laravel-Scout gerará um diretório de projeto aqui. No exemplo abaixo, o script Laravel-Scout criaria um diretório dentro da pasta Área de Trabalho.

cd /desktop

Execute o comando abaixo para executar o script Laravel-Scout. Ele gerará um aplicativo Dockerizado com o código padrão necessário.

curl -s https://laravel.build/laravel-scout-app | bash

Após a execução, altere seu diretório usando cd laravel-scout-app. Em seguida, execute o comando sail-up na pasta do projeto para iniciar os contêineres do Docker para o seu aplicativo.

Observação: em muitas distribuições Linux, talvez você precise executar o comando abaixo com o comando sudo para iniciar privilégios elevados.

./vendor/bin/sail up

Você pode encontrar um erro:

Erro afirmando que a porta está alocada.
Erro afirmando que a porta está alocada.

Para resolver isso, use a variável APP_PORT para especificar uma porta no comando sail up:

APP_PORT=3001 ./vendor/bin/sail up

Em seguida execute o comando abaixo para executar o aplicativo por meio do Artisan no servidor PHP.

php artisan serve
Servindo o aplicativo Laravel com o Artisan.
Servindo o aplicativo Laravel com o Artisan.

No seu navegador, vá até o aplicativo em execução em http://127.0.0.1:8000. O aplicativo exibirá a página de boas-vindas do Laravel na rota padrão.

Página de boas-vindas do aplicativo Laravel.
Página de boas-vindas do aplicativo Laravel.

Como adicionar o Laravel Scout ao aplicativo

No terminal, digite o comando para ativar o gerenciador de pacotes PHP Composer para adicionar o Laravel Scout ao projeto.

composer require laravel/scout

A seguir publique o arquivo de configuração do Scout usando o comando vendor:publish. O comando publicará o arquivo de configuração scout.php no diretório config do seu aplicativo.

 php artisan vendor:publish --provider="LaravelScoutScoutServiceProvider"

Agora modifique o arquivo .env padrão para que contenha um valor booleano SCOUT_QUEUE.

Esse valor permitirá que o Scout coloque as operações em fila, propiciando melhores tempos de resposta. Sem ele, os drivers do Scout, como o Meilisearch, não refletirão novos registros imediatamente.

SCOUT_QUEUE=true

Além disso, modifique a variável DB_HOST no arquivo .env para apontar para o seu host local e usar o banco de dados MySQL nos contêineres Docker.

DB_HOST=127.0.0.1

Como marcar um modelo e configurar o índice

O Scout não habilita modelos de dados pesquisáveis por padrão. Você deve marcar explicitamente um modelo como pesquisável usando a trait LaravelScoutSearchable.

Comece criando um modelo de dados para um aplicativo de demonstração Train e marcando-o como pesquisável.

Como criar um modelo

Para o aplicativo Train, você deverá reservar o espaço para os nomes de cada train disponível.

Execute o comando do Artisan abaixo para gerar a migração e dê a ela o nome de create_trains_table.

php artisan make:migration create_trains_table 
Criando uma migração chamada create_trains_table.
Criando uma migração chamada create_trains_table.

A migração será gerada em um arquivo cujo nome combina o nome especificado e o timestamp.

Abra o arquivo de migração localizado no diretório database/migrations/.

Para adicionar uma coluna de título, adicione o seguinte código após a coluna id() na linha 17. O código adicionará uma coluna de título.

$table->string('title');

Para aplicar a migração, execute o comando abaixo.

php artisan migrate
Aplicando a migração do Artisan.
Aplicando a migração do Artisan.

Após executar as migrações do banco de dados, crie um arquivo chamado Train.php no diretório app/Models/.

Como adicionar a trait LaravelScoutSearchable

Marque o modelo Train para pesquisa adicionando a trait LaravelScoutSearchable ao modelo, conforme mostrado abaixo.

<?php
namespace AppModels;
use IlluminateDatabaseEloquentModel;
use LaravelScoutSearchable;

class Train extends Model
{
    use Searchable;
    public $fillable = ['title'];

Além disso, você precisa configurar os índices de pesquisa substituindo o método searchable. O comportamento padrão do Scout manteria o modelo para corresponder ao nome da tabela do modelo.

Portanto, adicione o seguinte código ao arquivo Train.php abaixo do código do bloco anterior.

/**
     * Retrieve the index name for the model.
     *
     * @return string
    */
    public function searchableAs()
    {
        return 'trains_index';
   }
}

Como usar o Algolia com o Scout

Para a primeira pesquisa de texto completo com o Laravel Scout, você usará o driver Algolia, uma plataforma de software como serviço (SaaS) usada para pesquisar grandes quantidades de dados. Ele fornece um painel da web para que os desenvolvedores gerenciem seus índices de pesquisa e uma API robusta que você pode acessar por meio de um kit de desenvolvimento de software (SDK) em sua linguagem de programação preferida.

No aplicativo Laravel, você usará o pacote do cliente Algolia para PHP.

Como configurar o Algolia

Primeiro, você deve instalar o pacote do cliente de pesquisa PHP do Algolia para o seu aplicativo.

Execute o comando abaixo.

composer require algolia/algoliasearch-client-php

Em seguida, você deve definir o ID do aplicativo e as credenciais da chave secreta da API do Algolia no arquivo .env.

Usando o navegador, vá até o painel do Algolia para obter o ID do aplicativo e as credenciais da chave secreta da API.

Clique em Settings (Configurações) na parte inferior da barra lateral esquerda para navegar até a página Settings.

Em seguida, clique em API Keys (Chaves de API) na seção Team and Access (Equipe e acesso) da página Settings para visualizar as chaves da sua conta Algolia.

Página de chaves de API na Algolia Cloud.
Página de chaves de API na Algolia Cloud.

Na página Chaves de API, observe os valores Application ID (ID do aplicativo) e Admin API Key (Chave de API do administrador). Você usará essas credenciais para autenticar a conexão entre o aplicativo Laravel e o Algolia.

ID do aplicativo e chave de API do administrador.
ID do aplicativo e chave de API do administrador.

Adicione o código abaixo ao seu arquivo .env usando seu editor de código e substitua os espaços reservados pelos segredos correspondentes da API do Algolia.

ALGOLIA_APP_ID=APPLICATION_ID
ALGOLIA_SECRET=ADMIN_API_KEY

Além disso, substitua a variável SCOUT_DRIVER pelo código abaixo para alterar o valor de meilisearch para algolia. Essa alteração instrui o Scout a usar o driver do Algolia.

SCOUT_DRIVER=algolia

Como criar os controladores de aplicativo

No diretório app/Http/Controllers/, crie um arquivo TrainSearchController.php para armazenar um controlador para o aplicativo. O controlador listará e adicionará dados ao modelo Train.

Adicione o seguinte bloco de código ao arquivo TrainSearchController.php para criar o controlador.

<?php
namespace AppHttpControllers;
use IlluminateHttpRequest;
use AppHttpRequests;
use AppModelsTrain;

class TrainSearchController extends Controller
{
    /**
     * Get the index name for the model.
     *
     * @return string
    */
    public function index(Request $request)
    {
        if($request->has('titlesearch')){
            $trains = Train::search($request->titlesearch)
                ->paginate(6);
        }else{
            $trains = Train::paginate(6);
        }
        return view('Train-search',compact('trains'));
    }

    /**
     * Get the index name for the model.
     *
     * @return string
    */
    public function create(Request $request)
    {
        $this->validate($request,['title'=>'required']);

        $trains = Train::create($request->all());
        return back();
    }
}

Como criar as rotas do aplicativo

Nesta etapa, você criará as rotas para listar e adicionar novos trens ao banco de dados.

Abra o arquivo routes/web.php e substitua o código existente pelo bloco abaixo.

<?php

use IlluminateSupportFacadesRoute;
use AppHttpControllersTrainSearchController;

Route::get('/', function () {
    return view('welcome');
});

Route::get('trains-lists', [TrainSearchController::class, 'index']) -> name ('trains-lists');

Route::post('create-item', [TrainSearchController::class, 'create']) -> name ('create-item');

O código acima define duas rotas no aplicativo. A solicitação GET para a rota /trains-lists alista todos os dados de trens armazenados. A solicitação POST para a rota /create-item cria novos dados de train.

Como criar as visualizações do aplicativo

Crie um arquivo no diretório resources/views/ e nomeie-o Train-search.blade.php. O arquivo exibirá a interface de usuário para a funcionalidade de pesquisa.

Adicione o conteúdo do bloco de código abaixo ao arquivo Train-search.blade.php para criar uma única página para a funcionalidade de pesquisa.

<!DOCTYPE html>
<html>
<head>
    <title>Laravel - Laravel Scout Algolia Search Example</title>
    <link rel="stylesheet" type="text/css" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">
</head>
<body>
<div class="container">
    <h2 class="text-bold">Laravel Full-Text Search Using Scout </h2><br/>
    <form method="POST" action="{{ route('create-item') }}" autocomplete="off">
        @if(count($errors))
            <div class="alert alert-danger">
                <strong>Whoops!</strong> There is an error with your input.
                <br/>
                <ul>
                    @foreach($errors->all() as $error)
                    <li>{{ $error }}</li>
                    @endforeach
                </ul>
            </div>
        @endif

        <input type="hidden" name="_token" value="{{ csrf_token() }}">

        <div class="row">
            <div class="col-md-6">
                <div class="form-group {{ $errors->has('title') ? 'has-error' : '' }}">
                    <input type="text" id="title" name="title" class="form-control" placeholder="Enter Title" value="{{ old('title') }}">
                    <span class="text-danger">{{ $errors->first('title') }}</span>
                </div>
            </div>
            <div class="col-md-6">
                <div class="form-group">
                    <button class="btn btn-primary">Create New Train</button>
                </div>
            </div>
        </div>
    </form>

    <div class="panel panel-primary">
      <div class="panel-heading">Train Management</div>
      <div class="panel-body">
            <form method="GET" action="{{ route('trains-lists') }}">

                <div class="row">
                    <div class="col-md-6">
                        <div class="form-group">
                            <input type="text" name="titlesearch" class="form-control" placeholder="Enter Title For Search" value="{{ old('titlesearch') }}">
                        </div>
                    </div>
                    <div class="col-md-6">
                        <div class="form-group">
                            <button class="btn btn-primary">Search</button>
                        </div>
                    </div>
                </div>
            </form>

            <table class="table">
                <thead>
                    <th>Id</th>
                    <th>Train Title</th>
                    <th>Creation Date</th>
                    <th>Updated Date</th>
                </thead>
                <tbody>
                    @if($trains->count())
                        @foreach($trains as $key => $item)
                            <tr>
                                <td>{{ ++$key }}</td>
                                <td>{{ $item->title }}</td>
                                <td>{{ $item->created_at }}</td>
                                <td>{{ $item->updated_at }}</td>
                            </tr>
                        @endforeach
                    @else
                        <tr>
                            <td colspan="4">No train data available</td>
                        </tr>
                    @endif
                </tbody>
            </table>
            {{ $trains->links() }}
      </div>
    </div>
</div>
</body>
</html>

O código HTML acima contém um elemento de formulário com um campo de entrada e um botão para digitar o título train antes de você salvá-lo no banco de dados. O código também tem uma tabela HTML que exibe os detalhes idtitlecreated_at e updated_at de uma entrada train no banco de dados.

Como usar a pesquisa Algolia

Para visualizar a página, navegue até http://127.0.0.1:8000/trains-lists em seu navegador da web.

Dados do modelo train.
Dados do modelo train.

O banco de dados está vazio no momento, então você precisa inserir um título train de demonstração no campo de entrada e clicar em Create New Train para salvá-lo.

Inserção de uma nova entrada train.
Inserção de uma nova entrada train.

Para usar o recurso de pesquisa, digite uma palavra-chave de qualquer título train salvo no campo de entrada Enter Title For Search (Insira o título para pesquisa) e clique em Search.

Conforme mostrado na imagem abaixo, serão exibidas apenas as entradas de pesquisa que contenham a palavra-chave no título.

Usando o recurso de pesquisa para encontrar uma entrada train.
Usando o recurso de pesquisa para encontrar uma entrada train.

Meilisearch com Laravel Scout

O Meilisearch é um mecanismo de pesquisa de código aberto com foco em velocidade, desempenho e na experiência aprimorada do desenvolvedor. Compartilha vários recursos do Algolia, usando os mesmos algoritmos, estruturas de dados e pesquisa — mas com uma linguagem de programação diferente.

Os desenvolvedores podem criar e hospedar uma instância do Meilisearch em sua infraestrutura local ou na nuvem. O Meilisearch também tem uma oferta de nuvem beta semelhante ao Algolia para desenvolvedores que desejam usar o produto sem gerenciar sua infraestrutura.

No tutorial você já tem uma instância local do Meilisearch em execução em seus contêineres Docker. Agora você estenderá a funcionalidade do Laravel Scout para usar a instância do Meilisearch.

Para adicionar o Meilisearch ao aplicativo Laravel, execute o comando abaixo no terminal do seu projeto.

composer require meilisearch/meilisearch-php

Daí você precisa modificar as variáveis do Meilisearch no arquivo .env para configurá-lo.

Substitua as variáveis SCOUT_DRIVER, MEILISEARCH_HOST e MEILISEARCH_KEY no arquivo .env pelas abaixo.

SCOUT_DRIVER=meilisearch
MEILISEARCH_HOST=http://127.0.0.1:7700
MEILISEARCH_KEY=LockKey

A chave SCOUT_DRIVER especifica o driver que o Scout deve usar, ao passo que MEILISEARCH_HOST representa o domínio em que a instância do Meilisearch está sendo executada. Embora não seja necessário durante o desenvolvimento, recomendamos que você adicione MEILISEARCH_KEY na produção.

Observação: Insira um comentário com a ID e o segredo do Algolia quando usar o Meilisearch como driver preferencial.

Após concluir as configurações .env, você deve indexar os registros pré-existentes usando o comando Artisan abaixo.

php artisan scout:import "AppModelsTrain"

Laravel Scout com mecanismo de banco de dados

O mecanismo de banco de dados do Scout pode ser mais adequado para aplicativos que usam bancos de dados menores ou gerenciam cargas de trabalho menos intensas. Atualmente, o mecanismo de banco de dados é compatível com PostgreSQL e MySQL.

Esse mecanismo aplica cláusulas “where-like” e índices de texto completo ao seu banco de dados existente, permitindo encontrar os resultados de pesquisa mais relevantes. Você não precisa indexar seus registros ao usar o mecanismo de banco de dados.

Para usar o mecanismo de banco de dados, você deve definir a variável SCOUT_DRIVER .env para o banco de dados.

Abra o arquivo .env no aplicativo Laravel e altere o valor da variável SCOUT_DRIVER.

SCOUT_DRIVER = database

Após alterar seu driver para o banco de dados, o Scout passará a usar o mecanismo de banco de dados para pesquisa de texto completo.

Mecanismo de coleta com o Laravel Scout

Além do mecanismo de banco de dados, o Scout também oferece um mecanismo de coleta. Esse mecanismo usa cláusulas “where” e filtragem da coleta para extrair os resultados de pesquisa mais relevantes.

Ao contrário do mecanismo de banco de dados, o mecanismo de coleta é compatível com todos os bancos de dados relacionais que o Laravel também suporta.

Você pode usar o mecanismo de coleta definindo a variável de ambiente SCOUT_DRIVER como collection ou especificando manualmente o driver de coleta no arquivo de configuração do Scout.

SCOUT_DRIVER = collection

Explorer com Elasticsearch

Com a força das consultas do Elasticsearch, o Explorer é um driver moderno do Elasticsearch para o Laravel Scout. Ele oferece um driver Scout compatível e benefícios como armazenamento, pesquisa e análise de grandes quantidades de dados em tempo real. O Elasticsearch com Laravel apresenta resultados em milissegundos.

Para usar o driver Elasticsearch Explorer em seu aplicativo Laravel, você precisará configurar o arquivo boilerplate docker-compose.yml que o script Laravel-Scout gerou. Você adicionará as configurações adicionais para o Elasticsearch e reiniciará os contêineres.

Abra o arquivo docker-compose.yml e substitua seu conteúdo pelo seguinte.

# For more information: https://laravel.com/docs/sail
version: '3'
services:
    laravel.test:
        build:
            context: ./vendor/laravel/sail/runtimes/8.1
            dockerfile: Dockerfile
            args:
                WWWGROUP: '${WWWGROUP}'
        image: sail-8.1/app
        extra_hosts:
            - 'host.docker.internal:host-gateway'
        ports:
            - '${APP_PORT:-80}:80'
            - '${VITE_PORT:-5173}:${VITE_PORT:-5173}'
        environment:
            WWWUSER: '${WWWUSER}'
            LARAVEL_SAIL: 1
            XDEBUG_MODE: '${SAIL_XDEBUG_MODE:-off}'
            XDEBUG_CONFIG: '${SAIL_XDEBUG_CONFIG:-client_host=host.docker.internal}'
        volumes:
            - '.:/var/www/html'
        networks:
            - sail
        depends_on:
            - mysql
            - redis
            - meilisearch
            - mailhog
            - selenium
            - pgsql
            - elasticsearch

    mysql:
        image: 'mysql/mysql-server:8.0'
        ports:
            - '${FORWARD_DB_PORT:-3306}:3306'
        environment:
            MYSQL_ROOT_PASSWORD: '${DB_PASSWORD}'
            MYSQL_ROOT_HOST: "%"
            MYSQL_DATABASE: '${DB_DATABASE}'
            MYSQL_USER: '${DB_USERNAME}'
            MYSQL_PASSWORD: '${DB_PASSWORD}'
            MYSQL_ALLOW_EMPTY_PASSWORD: 1
        volumes:
            - 'sail-mysql:/var/lib/mysql'
            - './vendor/laravel/sail/database/mysql/create-testing-database.sh:/docker-entrypoint-initdb.d/10-create-testing-database.sh'
        networks:
            - sail
        healthcheck:
            test: ["CMD", "mysqladmin", "ping", "-p${DB_PASSWORD}"]
            retries: 3
            timeout: 5s
            
    elasticsearch:
        image: 'elasticsearch:7.13.4'
        environment:
            - discovery.type=single-node
        ports:
            - '9200:9200'
            - '9300:9300'
        volumes:
            - 'sailelasticsearch:/usr/share/elasticsearch/data'
        networks:
            - sail
    kibana:
        image: 'kibana:7.13.4'
        environment:
            - elasticsearch.hosts=http://elasticsearch:9200
        ports:
            - '5601:5601'
        networks:
            - sail
        depends_on:
            - elasticsearch
    redis:
        image: 'redis:alpine'
        ports:
            - '${FORWARD_REDIS_PORT:-6379}:6379'
        volumes:
            - 'sail-redis:/data'
        networks:
            - sail
        healthcheck:
            test: ["CMD", "redis-cli", "ping"]
            retries: 3
            timeout: 5s
    pgsql:
        image: 'postgres:13'
        ports:
            - '${FORWARD_DB_PORT:-5432}:5432'
        environment:
            PGPASSWORD: '${DB_PASSWORD:-secret}'
            POSTGRES_DB: '${DB_DATABASE}'
            POSTGRES_USER: '${DB_USERNAME}'
            POSTGRES_PASSWORD: '${DB_PASSWORD:-secret}'
        volumes:
            - 'sailpgsql:/var/lib/postgresql/data'
        networks:
            - sail
        healthcheck:
            test: ["CMD", "pg_isready", "-q", "-d", "${DB_DATABASE}", "-U", "${DB_USERNAME}"]
            retries: 3
            timeout: 5s
    meilisearch:
        image: 'getmeili/meilisearch:latest'
        ports:
            - '${FORWARD_MEILISEARCH_PORT:-7700}:7700'
        volumes:
            - 'sail-meilisearch:/meili_data'
        networks:
            - sail
        healthcheck:
            test: ["CMD", "wget", "--no-verbose", "--spider",  "http://localhost:7700/health"]
            retries: 3
            timeout: 5s
    mailhog:
        image: 'mailhog/mailhog:latest'
        ports:
            - '${FORWARD_MAILHOG_PORT:-1025}:1025'
            - '${FORWARD_MAILHOG_DASHBOARD_PORT:-8025}:8025'
        networks:
            - sail
    selenium:
        image: 'selenium/standalone-chrome'
        extra_hosts:
            - 'host.docker.internal:host-gateway'
        volumes:
            - '/dev/shm:/dev/shm'
        networks:
            - sail
networks:
    sail:
        driver: bridge
volumes:
    sail-mysql:
        driver: local
    sail-redis:
        driver: local
    sail-meilisearch:
        driver: local
    sailpgsql:
        driver: local
    sailelasticsearch:
        driver: local 

Em seguida, execute o comando abaixo para extrair a nova imagem do Elasticsearch que você adicionou ao arquivo docker-compose.yml.

docker-compose up

Daí execute o comando Composer abaixo para instalar o Explorer no projeto.

composer require jeroen-g/explorer

Você também precisa criar um arquivo de configuração para o driver do Explorer.

Execute o comando Artisan abaixo para gerar um arquivo explorer.config para armazenar as configurações.

php artisan vendor:publish --tag=explorer.config

O arquivo de configuração gerado acima estará disponível no diretório /config.

No arquivo config/explorer.php, você pode referenciar seu modelo usando a chave indexes.

'indexes' => [
        AppModelsTrain::class
],

Altere o valor da variável SCOUT_DRIVER no arquivo .env para elastic para configurar o Scout para usar o driver do Explorer.

SCOUT_DRIVER = elastic

Neste ponto, você usará o Explorer no modelo Train implementando a interface Explorer, que substituirá o método mappableAs().

Abra o arquivo Train.php no diretório App > Models e substitua o código existente pelo código abaixo.

<?php
namespace AppModels;
 
use IlluminateDatabaseEloquentFactoriesHasFactory;
use IlluminateDatabaseEloquentModel;
use JeroenGExplorerApplicationExplored;
use LaravelScoutSearchable;
 
class Train extends Model implements Explored
{
    use HasFactory;
    use Searchable;
 
    protected $fillable = ['title'];
 
    public function mappableAs(): array
    {
        return [
        	'id'=>$this->Id,
        	'title' => $this->title,
        ];
    }
} 

Com o código que você adicionou acima, agora você pode usar o Explorer para pesquisar texto no modelo Train.

Resumo

Para desenvolvedores PHP, o Laravel e complementos como o Scout tornam suave a integração de uma funcionalidade rápida e robusta de pesquisa de texto completo. Com o mecanismo de banco de dados, o mecanismo de coleta e os recursos do Meilisearch e do Elasticsearch, você pode interagir com o banco de dados do seu aplicativo e implementar mecanismos de pesquisa avançados em meros milissegundos.

O gerenciamento e a atualização impecáveis do seu banco de dados permite que você tenha uma experiência ideal, ao mesmo tempo que seu código permanece limpo e eficiente.

Com nossas soluções de hospedagem de aplicativos e bancos de dados, a Kinsta é o lugar ideal para ter todas as suas modernas necessidades de desenvolvimento com Laravel atendidas. Os primeiros $20 são por nossa conta.

Jeremy Holcombe Kinsta

Content & Marketing Editor at Kinsta, WordPress Web Developer, and Content Writer. Outside of all things WordPress, I enjoy the beach, golf, and movies. I also have tall people problems ;).