如何使用相互继承的Laravel雄辩模型使关系正常工作

时间:2019-05-04 20:29:44

标签: php laravel eloquent routing

我们有一个表invoices,其中有一个可为空的字段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,因为您知道我们不为形式发票指定编号,但只有在我们收取形式发票并将其转换为发票后才分配。

因此,我们的第一个问题是,是否有一种方法可以设置单个路由的路由参数需要绑定到哪一列,但这似乎是不可能的,因此我们选择了另一条路由。如果您看到一个允许我同时支持绑定数字和ID的解决方案,那么请让我知道:-)

无论如何,我们继续我们的工作,然后我们认为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 个答案:

答案 0 :(得分:1)

在我看来,您采取了一种非常复杂且round回的方式。 当您编写类似

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

Laravel知道{invoice}可与路由模型绑定一起使用,以直接获取Invoice实例,而不是通过将(Invoice $invoice)作为参数而不是($invoice)作为参数传递给id来传递ID。 / p>

如果您不想使用id查找模型怎么办?如您所知,您可以覆盖$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查询的更多信息,那么我在这里没有太多帮助。我以为可能是因为第三个参数(本地键),它应该是模型的主键,但对我来说没有意义。