Как заставить отношения работать должным образом, используя модели Laravel Eloquent, которые наследуются друг от друга - PullRequest
3 голосов
/ 04 мая 2019

У нас есть таблица invoices, которая имеет поле Nullable number.Наша бизнес-логика заключается в следующем.

Всякий раз, когда нам нужно взимать с клиента плату за обновление его услуг, мы высылаем ему счет-проформу.Счета-проформы распознаются атрибутом number как пустой.

Всякий раз, когда мы хотим преобразовать счета-проформы (вручную или автоматически, потому что клиент заплатил), все, что мы в основном должны сделать, это определить новый номер и обновитьinvoice.number.Эта функция прекрасно работает.

Теперь нам нужен маршрут GET /api/invoices/201900001, который, как вы правильно догадались, должен получить счет WHERE number = 201900001.

Итак, мы определили нашу модель счетаследующим образом:

class Invoice extends Model {
    public function getRouteKeyName() {
        return 'number';
    }
}

работает блестяще.Но я думаю, вы знаете, что еще нам нужно.Нам также необходимо иметь возможность получать счета-проформы по маршруту GET /api/proformas/1, где 1 фактически является id, а не number записи, поскольку вы знаете, что мы не присваиваем номер счетам-проформам, но назначаем только после того, как мы выставим счета-проформы и преобразуем их в счет-фактуру.

Таким образом, наш первый вопрос заключался в том, существует ли способ установить, к какому столбцу должен быть привязан параметр маршрута для отдельного маршрута, ноэто кажется невозможным, поэтому мы пошли другим путем.Если вы видите решение, которое позволяет мне поддерживать привязку к номеру и идентификатору в зависимости от маршрута, то непременно сообщите мне: -)

В любом случае, мы продолжили нашу работу, и тогда мы подумаличто ProformaInvoice на самом деле является специфическим подтипом объекта Invoice, поэтому мы пошли дальше и создали для этой цели новую модель:

Class ProformaInvoice extends Invoice {
    public function getRouteKeyName() {
        // override parent's behaviour back to the default
        return 'id';
    }
}

Вау - мы верили, что это должно было решить нашупроблемы, но это не так.Потому что угадайте, что - у нас также есть InvoiceElements, и они связаны с инвойсом.

Итак, у нас есть прекрасное отношение к модели инвойса:

Class Invoice extends Model {
    public function getRouteKeyName() {
        return 'number';
    }

    public function invoice_elements() {
        return $this->hasMany(InvoiceElement::class);
    }
}

Поскольку у нас теперь есть собственный ProformaInvoiceВ этой модели мы перенесли логику фактического начисления счета-проформы в этот класс (раньше это было на самой модели Invoice):

public function ProformInvoice extends Invoice {
    public function getRouteKeyName() {
        return 'id';
    }

    public function convertToInvoice() {
        $minNumber = (date('Y') * 100000) + 1;
        DB::statement(
            "UPDATE invoices inv
             JOIN (
                 SELECT IF(IFNULL(MAX(number)+1,1) < " . $minNumber . ", " . $minNumber . ", MAX(number) + 1) AS newNumber
                 FROM invoices
             ) t
             SET inv.number = t.newNumber, invoice_date = NOW(), updated_at = NOW()
             WHERE id = " . $this->id;
        );
        return Invoice::find($this->id);
    }
}

Мы думали, что мы действительно умны, потому что теперь наш CreateInvoiceControllerработает так, и имеет для нас смысл:

Class CreateInvoiceController extends Controller {
    public function create(CreateInvoiceRequest $request) {
        $invoice = null;
        DB::transaction(function() use($request, &invoice) {
            $proforma = ProformaInvoice::create([
                'due_date' => $request->get('due_date'),
                'subtotal' => $request->get('subtotal'),
                'vat' => $request->get('vat');
                'total' => $request->get('total');
            ]);

            $proforma->invoice_elements()->createMany($request->get('elements'));

            $invoice = $proforma->convertIntoInvoice();
        }, 1);

        return InvoiceResource::make($invoice);
    }
}

Хорошо, поэтому выполнение этого выбросило исключение, а именно Column not found: 1054 Unknown column 'proforma_invoice_id' in 'field list' (SQL: insert into 'invoice_elements'...

Это действительно имело смысл для меня, так как отношенияопределяется в модели Invoice и наследуется моделью ProformaInvoice.При попытке создания связанных моделей совершенно логично, что в нем не использовалось правильное имя столбца invoice_id, поскольку мы не определяли явно внешний ключ при определении отношения, поэтому я решил, что у нас есть быстрое решение, а именно, явноопределите внешний ключ в определении отношения:

Class Invoice extends Model {
    public function getRouteKeyName() {
        return 'number';
    }

    public function invoice_elements() {
        return $this->hasMany(InvoiceElement::class, 'invoice_id');
    }
}

К моему большому удивлению, это не решает проблему;Я по-прежнему получаю сообщение об ошибке, что столбец proforma_invoice_id не существует, хотя я явно устанавливаю определение внешнего ключа на invoice_id для его определения ...

Я что-то пропускаю или это ошибкав рамках?

Вы бы посоветовали другой путь, который имеет больше смысла для вас?Для меня это имеет смысл сейчас, но может быть другое решение ...

Я мог бы просто не привязывать модель, а сам загрузить модель в контроллере, но я хотел бы сохранитьиспользуйте как можно более чистый код и по возможности используйте привязку модели при настройке маршрутов.

Если вы попали сюда - СПАСИБО за чтение.

1 Ответ

1 голос
/ 05 мая 2019

Мне кажется, вы выбрали действительно сложный и окольный способ ведения дел.Когда вы пишете что-то вроде

Route::get('invoices/{invoice}', 'InvoiceController@show');

Laravel знает, что {invoice} можно использовать с привязкой модели маршрута, чтобы получить экземпляр Invoice напрямую, вместо того, чтобы передавать только идентификатор, указав (Invoice $invoice) в качестве параметра в методе контроллера вместоиз всего ($invoice).

Что если вы не хотите использовать идентификатор для поиска модели?Как вы знаете, вы можете переопределить атрибут $routeKeyName.Но вы все еще хотите использовать $id в некотором роде.

По моему мнению, вы должны сделать добавить еще одну привязку в файл RouteServiceProvider .

Пример.

# app/Invoice.php
class Invoice extends Model
{
    public function getRouteKeyName()
    {
        return 'number';
    }
    ...
}
# app/Providers/RouteServiceProvider.php
use App\Invoice;

class RouteServiceProvider extends ServiceProvider
{
    public function boot()
    {
        parent::boot();
        // bind {proforma_invoice} param to Invoice model
        Route::model('proforma_invoice', Invoice::class, function ($param) {
            // Since you want the id, we use findOrFail so it throws a 404 instead of a null.
            // You can customize the logic.
            // more info on https://laravel.com/docs/5.8/routing
            return Invoice::findOrFail($param);
        });
    }
}

# routes/api.php

// matches GET api/invoices/1234564
// tries to match invoice by its number attribute because of the routeKeyName override
Route::get('invoices/{invoice}', 'InvoiceController@showInvoice');
// matches GET api/proformas/1234564
// uses the definition in RouteServiceProvider to try and match an invoice
Route::get('proformas/{proforma_invoice}', 'InvoiceController@showProforma')
class InvoiceController extends Controller
{
    ...
    public function showProforma(Invoice $invoice) { ... }
    public function showInvoice(Invoice $invoice) { ... }
}

Таким образом, он использует только модель Invoice, и вам не придется жонглировать вещами, чтобы заставить работать отношения.

Безбольше информации о SQL-запросе, который делает ваш метод отношений неудачным, я не могу помочь.Я думал, что это может быть из-за третьего параметра (локальный ключ), который должен быть первичным ключом модели, но это не имеет смысла для меня.

...