quarta-feira, 24 de fevereiro de 2021

docker-compose.yml para desenvolvimento em Laravel


version: '3.7'
# Networks
networks:
  # Internal network
  internal:
    driver: bridge
# Volumes
volumes:
  # MySQL volume
  sandbox-mysql_data:
    driver: local
services:
  # api
  sandbox-apache:
    image: webdevops/php-apache-dev:7.4
    container_name: sandbox-apache
    networks:
      - internal
    working_dir: /var/www/app
    volumes:
      - .:/var/www/app
    depends_on:
      - sandbox-mysql
    environment:
      - WEB_DOCUMENT_ROOT=/var/www/app/public
      - PHP_DEBUGGER="xdebug"
      - XDEBUG_REMOTE_AUTOSTART=On
      - XDEBUG_REMOTE_CONNECT_BACK=Off
      - XDEBUG_REMOTE_HOST=host.docker.internal
      - XDEBUG_REMOTE_PORT=9090
      - XDEBUG_PROFILER_ENABLE=Off
      - PHP_DATE_TIMEZONE=UTC
      - PHP_DISPLAY_ERRORS=0
      - WEB_DOCUMENT_INDEX=index.php
      - PHP_MEMORY_LIMIT=2048M
      - PHP_MAX_EXECUTION_TIME=86400
      - PHP_POST_MAX_SIZE=500M
      - PHP_UPLOAD_MAX_FILESIZE=500M
    expose:
      - 9000
    ports:
      - 8080:80
  # MySQL
  sandbox-mysql:
    image: mysql:5.7
    container_name: sandbox-mysql
    networks:
      - internal
    working_dir: /var/www/app
    volumes:
      - sandbox-mysql_data:/var/lib/mysql
      - .:/var/www/app
    environment:
      - MYSQL_ROOT_PASSWORD=root
      - MYSQL_DATABASE=database
      - MYSQL_USER=user
      - MYSQL_PASSWORD=root
    ports:
      - 3306:3306

terça-feira, 23 de fevereiro de 2021

Desenvolver Página Master/Detail em Laravel

 Master-Detail Form in Laravel + jQuery: Create Order With Products – Quick Admin Panel

It’s pretty easy to create a simple form in Laravel. But it’s harder to make it dynamic – the most common case is parent-child elements, like creating invoice and adding items dynamically in the same form. In this article, we will create a similar example – creating an order and adding products to it.


Database Migrations and Models

First, database structure in Laravel – three migration files:

Schema::create('products', function (Blueprint $table) {
    $table->increments('id');
    $table->string('name');
    $table->decimal('price', 15, 2)->nullable();
    $table->timestamps();
});

Schema::create('orders', function (Blueprint $table) {
    $table->increments('id');
    $table->string('customer_name');
    $table->string('customer_email')->nullable();
    $table->timestamps();
});

Schema::create('order_product', function (Blueprint $table) {
    $table->unsignedInteger('order_id');
    $table->foreign('order_id')->references('id')->on('orders');
    $table->unsignedInteger('product_id');
    $table->foreign('product_id')->references('id')->on('products');
    $table->integer('quantity');
});

As you can see, there’s a many-to-many relationship between order and product, and pivot table also contains integer field quantity.

Here’s how it looks in Laravel models.

app/Product.php:

class Product extends Model
{
    protected $fillable = [
        'name',
        'price',
    ];
}

app/Order.php:

class Order extends Model
{
    protected $fillable = [
        'customer_name',
        'customer_email',
    ];

    public function products()
    {
        return $this->belongsToMany(Product::class)->withPivot(['quantity']);
    }
}

As you can see, we add withPivot([‘quantity’]) to be able to manipulate that field easier with Eloquent, will show that later.

Now, if we run these migrations and then reverse engineer the database with MySQL Workbench, we have this structure:

Laravel MySQL Order Products database


Create Order Form

Now it’s time to build a form to create order, here’s the screenshot again:

Laravel master detail form

First, Controller method with passing available products from the database.

app/Http/Controllers/Admin/OrdersController.php:

public function create()
{
    $products = Product::all();
    return view('admin.orders.create', compact('products'));
}

Simple, right? Now, let’s go to the Blade file.

Notice: This form was actually pre-generated by QuickAdminPanel, but you can build your own manually.

So, we have this in resources/views/admin/orders/create.blade.php. I will intentionally skip the customer name/email fields, and focus only on products:

<form action="{{ route("admin.orders.store") }}" method="POST">
    @csrf

    {{-- ... customer name and email fields --}}

    <div class="card">
        <div class="card-header">
            Products
        </div>

        <div class="card-body">
            <table class="table" id="products_table">
                <thead>
                    <tr>
                        <th>Product</th>
                        <th>Quantity</th>
                    </tr>
                </thead>
                <tbody>
                    <tr id="product0">
                        <td>
                            <select name="products[]" class="form-control">
                                <option value="">-- choose product --</option>
                                @foreach ($products as $product)
                                    <option value="{{ $product->id }}">
                                        {{ $product->name }} (${{ number_format($product->price, 2) }})
                                    </option>
                                @endforeach
                            </select>
                        </td>
                        <td>
                            <input type="number" name="quantities[]" class="form-control" value="1" />
                        </td>
                    </tr>
                    <tr id="product1"></tr>
                </tbody>
            </table>

            <div class="row">
                <div class="col-md-12">
                    <button id="add_row" class="btn btn-default pull-left">+ Add Row</button>
                    <button id='delete_row' class="pull-right btn btn-danger">- Delete Row</button>
                </div>
            </div>
        </div>
    </div>
    <div>
        <input class="btn btn-danger" type="submit" value="{{ trans('global.save') }}">
    </div>
</form>

So, we’re building a table of products here, and to make it dynamic we use approach I’ve found in this Bootstrap Snippet:

  • We prefill the first row of the table with all the fields
  • We also create new empty row which is invisible to user
  • Button “Add row” will then duplicate last row of the table and create one more empty invisible row
  • Button “Delete row” will then delete last row of the table
  • Every row’s input variables will be array, so for POST result we will have array of products[] and quantities[]

Here’s the jQuery part of this – put it in wherever you have your JavaScript section:

  $(document).ready(function(){
    let row_number = 1;
    $("#add_row").click(function(e){
      e.preventDefault();
      let new_row_number = row_number - 1;
      $('#product' + row_number).html($('#product' + new_row_number).html()).find('td:first-child');
      $('#products_table').append('<tr id="product' + (row_number + 1) + '"></tr>');
      row_number++;
    });

    $("#delete_row").click(function(e){
      e.preventDefault();
      if(row_number > 1){
        $("#product" + (row_number - 1)).html('');
        row_number--;
      }
    });
  });

Saving Order and Products Data

Let’s get back to our app/Http/Controllers/Admin/OrdersController.php, its store() method will look like this:

public function store(StoreOrderRequest $request)
{
    $order = Order::create($request->all());

    $products = $request->input('products', []);
    $quantities = $request->input('quantities', []);
    for ($product=0; $product < count($products); $product++) {
        if ($products[$product] != '') {
            $order->products()->attach($products[$product], ['quantity' => $quantities[$product]]);
        }
    }

    return redirect()->route('admin.orders.index');
}

What do we have here?

  • First, we save the order’s main data into $order;
  • Then, we take products/quantities array, putting empty arrays as values in case they are missing;
  • We iterate through products and if product ID is not empty we use the attach() method of many-to-many relationships, adding a value of quantity as extra parameter (read more about many-to-many in my popular article).

And, that’s it with the data saving, order with products is saved in the database!


View Orders with Products in List

Final thing in this article – we have a list of orders, and one of the columns should be products from that order.

How to list those products?

app/Http/Controllers/Admin/OrdersController.php:

public function index()
{
    $orders = Order::with('products')->get();
    return view('admin.orders.index', compact('orders'));
}

And then – resources/views/admin/orders/index.blade.php – every row of the table should look like this:

@foreach($orders as $order)
    <tr data-entry-id="{{ $order->id }}">
        <td>
            {{ $order->id ?? '' }}
        </td>
        <td>
            {{ $order->customer_name ?? '' }}
        </td>
        <td>
            {{ $order->customer_email ?? '' }}
        </td>
        <td>
            <ul>
            @foreach($order->products as $item)
                <li>{{ $item->name }} ({{ $item->pivot->quantity }} x ${{ $item->price }})</li>
            @endforeach
            </ul>
        </td>
        <td>
            {{-- ... buttons ... --}}
        </td>

    </tr>
@endforeach

As you can see, we’re using $item->pivot->quantity to get that additional column from pivot table.


And, that’s it with this mini-project!
I’ve put the finished version on Github here: https://github.com/LaravelDaily/Laravel-Master-Detail-Form

There you will find some more functionality, like editing the orders, and full QuickAdminPanel-based login/permissions system.

quinta-feira, 4 de fevereiro de 2021

Laravel - Padrões de nomenclaturas - convenções

 Naming Conventions - Laravel Guidelines (xqsit.github.io)

Follow Laravel naming conventions

Follow PSR standards.

Also, follow naming conventions accepted by Laravel community:

WhatHowGoodBad
ControllersingularArticleControllerArticlesController
Routepluralarticles/1article/1
Named routesnake_case with dot notationusers.show_activeusers.show-active, show-active-users
ModelsingularUserUsers
hasOne or belongsTo relationshipsingulararticleCommentarticleComments, article_comment
All other relationshipspluralarticleCommentsarticleComment, article_comments
Tablepluralarticle_commentsarticle_comment, articleComments
Pivot tablesingular model names in alphabetical orderarticle_useruser_article, articles_users
Table columnsnake_case without model namemeta_titleMetaTitle; article_meta_title
Model propertysnake_case$model->created_at$model->createdAt
Foreign keysingular model name with _id suffixarticle_idArticleId, id_article, articles_id
Primary key-idcustom_id
Migration-2017_01_01_000000_create_articles_table2017_01_01_000000_articles
MethodcamelCasegetAllget_all
Method in resource controllertablestoresaveArticle
Method in test classcamelCasetestGuestCannotSeeArticletest_guest_cannot_see_article
VariablecamelCase$articlesWithAuthor$articles_with_author
Collectiondescriptive, plural$activeUsers = User::active()->get()$active, $data
Objectdescriptive, singular$activeUser = User::active()->first()$users, $obj
Config and language files indexsnake_casearticles_enabledArticlesEnabled; articles-enabled
Viewsnake_caseshow_filtered.blade.phpshowFiltered.blade.php, show-filtered.blade.php
Configsnake_casegoogle_calendar.phpgoogleCalendar.php, google-calendar.php
Contract (interface)adjective or nounAuthenticatableAuthenticationInterface, IAuthentication
TraitadjectiveNotifiableNotificationTrait

O que significa o erro "Execução de scripts foi desabilitada neste sistema"?

  O que significa o erro "Execução de scripts foi desabilitada neste sistema"? Execute Set-ExecutionPolicy -ExecutionPolicy Remote...