Подтвердить что ты не робот

Laravel 5.1 Eloquent ORM случайным образом возвращающ неправильное отношение - * главное уточнение *

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

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

Проблема, с которой я столкнулась, очень странная, и я считаю, что это может быть какая-то ошибка Laravel.

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

Это соответствующие части моделей в проекте:

class Order extends Model
{
    public function customer()
    {
        return $this->belongsTo('App\Customer');
    }
}

class Customer extends Model
{
    public function orders()
    {
        return $this->hasMany('App\Order');
    }

    public function user()
    {
        return $this->belongsTo('App\User');
    }
}

class User extends Model
{ 
    public function customer()
    {
        return $this->hasOne('App\Customer');
    }
}

Это миграция базы данных для вышеописанного (отредактировано для краткости):

   Schema::create('users', function (Blueprint $table) {
        $table->increments('id');
        $table->string('first_name');
        $table->string('last_name');
        $table->string('email')->unique();
        $table->string('password', 60);
        $table->boolean('active');
        $table->rememberToken();
        $table->timestamps();
        $table->softDeletes();
    });

    Schema::create('customers', function(Blueprint $table)
    {
        $table->increments('id');
        $table->integer('user_id')->nullable->index();
        $table->string('first_name');
        $table->string('last_name');
        $table->string('telephone')->nullable();
        $table->string('mobile')->nullable();
        $table->timestamps();
        $table->softDeletes();
    });

    Schema::create('orders', function(Blueprint $table)
    {
        $table->increments('id');
        $table->integer('payment_id')->nullable()->index();
        $table->integer('customer_id')->index();
        $table->integer('staff_id')->nullable()->index();
        $table->decimal('total', 10, 2);
        $table->timestamps();
        $table->softDeletes();
    });

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

Вот событие OrderSuccess (отредактировано для краткости):

namespace App\Events;

use App\Events\Event;
use App\Order;
use Illuminate\Queue\SerializesModels;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;


class OrderSuccess extends Event
{
    use SerializesModels;

    public $order;

    /**
     * Create a new event instance.
     *
     * @return void
     */
    public function __construct(Order $order)
    {
        $this->order = $order;
    }
}

Как видно, этому событию передается объект модели Order.

Вот обработчик события (отредактированный для краткости):

/**
 * Handle the event.
 *
 * @param  OrderSuccess  $event
 * @return void
 */
public function handle(OrderSuccess $event)
{
    // set order to paid
    $order = $event->order;
    $order->paid = date('Y-m-d H:i:s');
    $order->save();

    if(!is_null($order->customer->user)) {

        App_log::add('customer_order_success_email_sent', 'Handlers\Events\OrderSuccessProcess\handle', $order->id, print_r($order->customer, true).PHP_EOL.print_r($order->customer->user, true));

        // email the user the order confirmation
        Mail::send('emails.order_success', ['order' => $order], function($message) use ($order)
        {
            $message->to($order->customer->user->email, $order->customer->first_name.' '.$order->customer->last_name)->subject('Order #'.$order->id.' confirmation');
        });
    }

}

Существует проверка того, не является ли объект $order->customer->user недействительным и, если true, отправляется подтверждение заказа. Если он является нулевым (что часто бывает), то подтверждение не отправляется.

Как видно из вышеизложенного, я добавил журнал для записи объектов при отправке письма. Вот пример ошибочного (опять-таки, усеченного для краткости):

App\Customer Object
(
[attributes:protected] => Array
    (
        [id] => 10412
        [user_id] => 
        [first_name] => Joe
        [last_name] => Bloggs
        [telephone] => 0123456789
        [created_at] => 2015-09-14 13:09:45
        [updated_at] => 2015-10-24 05:00:01
        [deleted_at] => 
    )

[relations:protected] => Array
    (
        [user] => App\User Object
            (
                [attributes:protected] => Array
                    (
                        [id] => 1206
                        [email] => [email protected]
                        [password] => hashed
                        [remember_token] => 
                        [created_at] => 2015-09-19 09:47:16
                        [updated_at] => 2015-09-19 09:47:16
                        [deleted_at] => 
                    )
            )

    )

[morphClass:protected] => 
[exists] => 1
[wasRecentlyCreated] => 
[forceDeleting:protected] => 
)

App\User Object
(
[attributes:protected] => Array
    (
        [id] => 1206
        [email] => [email protected]
        [password] => hashed
        [remember_token] => 
        [created_at] => 2015-09-19 09:47:16
        [updated_at] => 2015-09-19 09:47:16
        [deleted_at] => 
    )

[morphClass:protected] => 
[exists] => 1
[wasRecentlyCreated] => 
[forceDeleting:protected] => 
)

Как вы можете видеть, для Customer нет user_id, и все же Laravel вернул объект User.

Более того, если я вручную запускаю то же самое событие OrderSuccess, это не воспроизводится - он не отправляет электронное письмо и не загружает объект User.

Как я уже говорил, эта проблема происходит очень редко - в среднем в среднем около 40 заказов в день через колл-центр для клиентов без учетной записи пользователя, а выделенная проблема может возникать только один или два раза в неделю.

Я не достаточно знаком с Laravel, чтобы узнать, что может быть проблемой здесь - это какая-то форма кэширования модели, проблема с Eloquent ORM или какой-либо другой гремлин в системе?

Любые идеи оценены - я могу опубликовать эту проблему в трееере Laravel github, если она кажется некоторой формой ошибки.

Обновление. Относительно некоторых ответов/комментариев, которые были предложены, я попытался удалить любые потенциальные проблемы ОРВ, чтобы получить данные вручную, например:

$customer = Customer::find($order->customer_id);
$user = User::find($customer->user_id);

if(!is_null($user)) {
    // send email and log actions etc
}

Вышеупомянутые все еще производят одни и те же случайные результаты - не связанные пользователи получаются, даже если у клиента нет user_id (в этом случае это NULL).

Обновление 2 Поскольку первое обновление никоим образом не помогло, я вернулся к использованию исходного подхода Eloequent. Чтобы попробовать другое решение, я извлек код события из обработчика событий и поместил его в свой контроллер - раньше я запускал событие OrderSuccess с помощью Event::fire(new OrderSuccess ($order));, и вместо этого я прокомментировал эту строку и просто поместил код обработчика события в метод контроллера:

$order = Order::find($order_id);

//Event::fire(new OrderSuccess ($order));

// code from the above event handler
$order->paid = date('Y-m-d H:i:s');
$order->save();

if(!is_null($order->customer->user)) {

    App_log::add('customer_order_success_email_sent', 'Handlers\Events\OrderSuccessProcess\handle', $order->id, print_r($order->customer, true).PHP_EOL.print_r($order->customer->user, true));

    // email the user the order confirmation
    Mail::send('emails.order_success', ['order' => $order], function($message) use ($order)
    {
        $message->to($order->customer->user->email, $order->customer->first_name.' '.$order->customer->last_name)->subject('Order #'.$order->id.' confirmation');
    });
}

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

Единственный возможный вывод, который я могу достичь, - это какая-то ошибка в системе событий Laravel, которая каким-то образом искажает переданный объект. Или может быть что-то еще в игре?

Обновление 3 Кажется, я преждевременно заявлял, что мой код за пределами события исправил проблему - на самом деле, через мой журнал, за последние 2 дня я увидел, что было отправлено еще некорректное подтверждение заказа (всего 5, после почти 3 недель без проблем).

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

Я также заметил, что каждый из проблемных заказов был оплачен наличными и кредитными картами - большинство из них только наличные. Я смотрел дальше на это, и идентификаторы пользователей на самом деле являются идентификаторами связанных кредитных транзакций!

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

Таким образом, проблема все еще случайна или, по-видимому, так. Событие погашения кредита запускается следующим образом:

Event::fire(new CreditRedemption( $credit, $order ));

Вышеупомянутое вызывается непосредственно перед моим событием OrderSuccess - как вы можете видеть, оба события передаются объекту модели $order.

Мой обработчик событий CreditRedemption выглядит следующим образом:

public function handle(CreditRedemption $event)
{
    // make sure redemption amount is a negative value
    if($event->credit < 0) {
        $amount = $event->credit;
    }
    else {
        $amount = ($event->credit * -1);
    }

    // create the credit transaction
    $credit_transaction = New Credit_transaction();
    $credit_transaction->transaction_type = 'Credit Redemption';
    $credit_transaction->amount = $amount; // negative value
    $credit_transaction->customer_id = $event->order->customer->id;
    $credit_transaction->order_id = $event->order->id;

    // record staff member if appropriate
    if(!is_null($event->order->staff)) {
        $credit_transaction->staff_id = $event->order->staff->id;
    }

    // save transaction
    $credit_transaction->save();

    return $credit_transaction;
}

$credit_transaction->save(); генерирует id в моей таблице credit_transactions, которая каким-то образом используется Laravel для извлечения пользовательского объекта. Как видно из вышеописанного обработчика, я не обновляю свой объект $order в любой точке.

Как использует Laravel (помните, все еще случайно, около 50% времени) идентификатор моего вновь созданного $credit_transaciton, чтобы заполнить объект модели $order->customer->user?

4b9b3361

Ответ 1

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

Оригинальная логика

$customer = Customer::find($order->customer_id);
$user = User::find($customer->user_id);

if(!is_null($user)) {
    // send email and log actions etc
}

Пересмотренная логика

Поскольку клиенты user_id могут быть нулевыми, может быть более эффективным ограничить возвращаемых клиентов теми, у кого есть user_id. Это может быть достигнуто с помощью метода whereNotNull(). Затем мы можем проверить, был ли возвращен клиент, и если да, отправьте электронное письмо и т.д.

$customer = Customer::whereNotNull('user_id')->find($order->customer_id); 

if (!$customer->isEmpty()) { 
    // send email and log actions etc 
}

Не предоставляя приложению возможность вернуть клиента с нулевым значением user_id, это, надеюсь, решит вашу проблему, но, к сожалению, он не освещает, почему это происходит в первую очередь.

Ответ 2

Я не уверен, что ваши миграции правильно определены по сравнению с вашими моделями. Если вы используете отношения belongsTo и hasOne, вы должны использовать ссылки на внешние ключи в миграциях.

Schema::create('customers', function(Blueprint $table)
    {
        $table->increments('id');
        $table->integer('user_id')->nullable();
        $table->foreign('user_id')->references('id')->on('users');

        $table->string('first_name');
        $table->string('last_name');
        $table->string('telephone')->nullable();
        $table->string('mobile')->nullable();
        $table->timestamps();
        $table->softDeletes();
    });



Schema::create('orders', function(Blueprint $table)
{
   $table->increments('id');
   $table->integer('payment_id')->nullable()->index();

   $table->integer('customer_id')->nullable();
   $table->foreign('customer_id')->references('id')->on('customers');
   $table->integer('staff_id')->nullable()->index();
   $table->decimal('total', 10, 2);
   $table->timestamps();
   $table->softDeletes();
    });

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

Шаг 1: сначала сохраните клиента.

$customer->save();

Шаг 2: Теперь мы установим user_id на клиенте, если он существует. Для этого вы можете получить объект пользователя в $user, а затем просто вызвать

$customer->user->save($user);

Приведенный выше код автоматически установит user_id в таблице клиентов

Затем я проверю, существует ли запись пользователя ниже:

$user_exists = $order->customer()->user();

if($user_exists)
{
    //email whatever
}

Ответ 3

Не должны ли ваши миграции иметь

->unsigned()

например:

$table->integer('user_id')->unsinged()->index();

Как упоминалось в Laravel Doc?

Laravel также обеспечивает поддержку для создания ограничений внешнего ключа, которые используются для принудительной ссылочной целостности на уровне базы данных. Например, пусть задает столбец user_id в таблице сообщений, который ссылается на столбец id в таблице пользователей. http://laravel.com/docs/5.1/migrations#foreign-key-constraints

Ответ 4

Нет необходимости иметь FK. Eloquent может создавать отношения, основанные на имени столбцов.

Измените имя полей. Имя поля должно соответствовать имени таблицы с суффиксом "_id". В таблице клиентов user_id должен быть user_id. В заказах customer_id должен быть customer_id.

Вы можете попробовать передать имя поля, с которым хотите присоединиться:

class Order extends Model
{
    public function customer()
    {
        return $this->belongsTo('App\Customer', 'foreign_key', 'id');
    }
}

Я не очень продвинутый пользователь с Laravel, поэтому это может не сработать. У меня была такая же проблема, и я решил ее, переименовав все модели и столбцы в соответствии с именами таблиц (с "s" ).

Ответ 5

поле user_id в таблице клиентов должно быть нулевым.

$table->integer('user_id')->index()->nullable();