trevorpan's avatar

How do you instantiate variables in an event based markdown mail?

Hi,

I've been banging up against a wall.

When reviewing this section of the docs: https://laravel.com/docs/5.8/mail#view-data

It appears you can import the class, set a public property, call $this->order = $order and voila! now you have the event based order available in the markdown mail.

https://laracasts.com/series/laravel-from-scratch-2018/episodes/32 That video shows a great technique of tucking the mail::to function in the notificationhandle().

however, no matter what I do, if I try to add job to the mailable (in addition to $order) I get these errors saying in the notification handle(), we expected 2 but got 1.

job is not a column in the orders table, it holds the user_id which then goes to a pivot table with job_id, order_id.

Thank you ~

0 likes
18 replies
Jaytee's avatar

Could you show us your code please. And the exact error that Laravel spits out.

trevorpan's avatar

good morning, thank you for checking this out. Just been driving me crazy...

here's the error:

Too few arguments to function App\Listeners\SendOrderPlacedNotification::handle(), 1 passed and exactly 2 expected
//event
class OrderPlaced
{
    use Dispatchable, InteractsWithSockets, SerializesModels;

    /**
     * The order instance.
     *
     * @var Order $order
     */
    public $order;


    /**
     * Create a new event instance.
     *
     * @return void
     */
    public function __construct(Order $order)
    {
        $this->order = $order;
    }
class OrderPlacedMail extends Mailable
{
    use Queueable, SerializesModels;


    /**
     * The job instance.
     *
     * @var Job
     */
    public $job;

    /**
     * The order instance.
     *
     * @var Order $order
     */
    public $order;

    /**
     * Create a new message instance.
     *
     * @param Job $job
     * @param object $order
     * @return void
     */
    public function __construct($order, $job)
    {
        $this->order = $order;
        $this->job = $job;
    }

    /**
     * Build the message.
     *
     * @return $this
     */
    public function build()
    {
        return $this->markdown('emails.order-confirmation-email')
                    ->subject("Your BidBird Order")
                    ->with([
                        'jobTitle' => $this->job->jobtitle,
                        'orderConfirmation_number' => $this->order->confirmation_number
                    ]);

    }
class SendOrderPlacedNotification implements ShouldQueue
{

    /**
     * Create the event listener.
     *
     * @return void
     */
    public function __construct()
    {
        //

    }

    /**
     * Handle the event.
     *
     * @param Job $jobTitle
     * @param  object  $event
     * @return void
     */
    public function handle(OrderPlaced $event, $jobTitle)
    {
        Mail::to($event->order->user->email)->send(
            new OrderPlacedMail($event->order, $jobTitle)
        );
    }
class Order extends Model
{
    // private $job;
    //    private $confirmation_number;
    protected $guarded = [];

    protected $dispatchesEvents = [
        'created' => OrderPlaced::class
    ];


    // belongsTo

    public function job()
    {
        return $this->belongsTo(Job::class);
    }
Snapey's avatar

your listener receives the event. Your event should contain the public properties that you want your listener to use

trevorpan's avatar

@snapey

Ahh man. Criminy. So, just add the public properties in the event!

Do you personally add them in both places (event and mailable)? I was referring to the docs where it's shown in the mailable with no reference to the event itself.

Do you find that confusing?

Snapey's avatar

I guess most mailables are not created from within event listeners?

Snapey's avatar

Problem is, your event receives the instance of the model through its constructor - and nothing else.

Not sure where you are getting $job / $jobTitle from?

public function handle(OrderPlaced $event, $jobTitle)
                                           ^^^^^^^^^
trevorpan's avatar

@snapey

Yea, that's partly why it's been so frustrating.

The docs say here using the with() method you can assign e.g. $jobTitle. At least, how I read it and then it's available in the view. Order is available and effective, passes the the tests, etc, if I comment out the job.

https://laravel.com/docs/5.8/mail#view-data

Isn't the constructor for instantiating? when you say nothing else what would you do in a situation like that?

Snapey's avatar

Get the sequence.

Model created --> Event fires --> Listener catches --> mailable generated

As you are observing the model being created, you are relying on how eloquent generates the event. It does not know about anything else other than the model concerned.

Yes, of course, you can pass data to the mailable to use in the markdown... but only data that is available to the thing creating the mailable, or that the mailable can create itself.

Your listener says

public function handle(OrderPlaced $event, $jobTitle)

Yes, it gets an instance of OrderPlaced event, but where do you think $jobTitle comes from?

You are asking for data in the listener that would simply have to come out of thin air?

1 like
trevorpan's avatar

@snapey

After much trial and error I've settled on using OrderPlaced event as I think it more accurately describes what happened. A BidReserve or a Bid make up orders. So I think having a Bid or BidReserve event is not really accurate.

Does that seem legit?

So, I've finally come across a major issue, which your quote explains

Model created --> Event fires --> Listener catches --> mailable generated

My issue is not instantiating variables for a markdown mail but accessing a pivot table from the order.

class CreateOrdersTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('orders', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->unsignedBigInteger('user_id');
            $table->string('confirmation_number');
            $table->integer('amount');
            $table->string('card_last_four');
            $table->timestamps();
        });
    }
class CreateBidReservesTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('bidreserves', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->unsignedBigInteger('job_id');
            $table->unsignedBigInteger('order_id')->nullable();
            $table->foreign('job_id')->references('id')->on('jobs');
            $table->bigInteger('amount');
            $table->timestamps();
        });
    }

It seems like $order->bidReserve->job->jobtitle would work, but it does not. It gives: Trying to get property 'job' of non-object

I gave this a go in the Order.php model. Where's this going wrong?

    public function bidReserve()
    {
        return $this->belongsTo(BidReserve::class)->wherePivot('job_id', 'order_id');
    }
Snapey's avatar

If bidReserve carries the foreign key to the Order then

BidReserve belongsTo Order / Order hasOne BidReserve

or

BidReserve belongsTo Order / Order hasMany BidReserve

Please check your relationships, and try it out in tinker so that you can be sure all your relationships work as expected.

trevorpan's avatar

Hi @snapey

My test keeps saying: ErrorException: Trying to get property of non-object

This mailable action only fails if trying to access data through the pivot table. e.g. dd($order->bidReserve->job->jobtitle);

id  job_id order_id amount    created_at                 updated_at
333   3     NULL    500       2019-10-06 21:08:34       2019-10-06 21:08:34

If it's changed to dd($order); It gives the proper data in my test:

  #original: array:7 [
    "amount" => 500
    "user_id" => 1
    "confirmation_number" => "ORDERCONFIRMATION1234"
    "card_last_four" => "1234"
    "updated_at" => "2019-10-07 01:03:55"
    "created_at" => "2019-10-07 01:03:55"
    "id" => 1
  ]

I've tried all your concepts above, even with hasOneThrough, hasManyThrough to no avail.

It seems like the pivot table bidreserves is causing issues. Is it a naming issue? e.g. it's not jobs_orders There is this in the BidReserve model: public $table = "bidreserves";

Basically, the orders table tallies amount and is linked to a user. The BidReserves table has the order_id. Should these relations be hasMany? to utilize pivot tables?

This is in the docs: return $this->belongsToMany('App\Role')->withPivot('column1', 'column2'); was not sure if this applies to this situation.

Any ideas?

Snapey's avatar

Is this bidreserves? (you don't say)

id  job_id order_id amount    created_at                 updated_at
333   3     NULL    500       2019-10-06 21:08:34       2019-10-06 21:08:34

order_id is null so this bidreserve is never going to relate to an order

trevorpan's avatar

@snapey

Sorry, just saw you responded.

Yea, that's the bidreserve. All I can think is that the update does not occur in time for the mailable to catch it and utilize the relationship.

How would you delay the update so the mailable can use it? Can't update the bidreserve till the order is created...

    /**
     * Store a newly created resource in storage.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     */
    public function store($jobId)
    {

        $job = Job::Incomplete()->findOrFail($jobId);

        $this->validate(request(), [
            'amount' => ['required', 'integer', 'min:1'],
            'stripeToken' => ['required']
        ]);

        // Charging the customer
        try {
            $bidReserve = $job->bidReserves()->create([
                'amount' => request('amount'),
                'job_id' => $jobId,
            ]);

            $order = $bidReserve->complete($this->paymentGateway, request('stripeToken'), auth()->user()->id);

            DB::transaction(function () use($order, $job, $bidReserve) {

                DB::table('bidreserves')
                    ->where([
                        ['job_id', $job->id],
                        ['created_at', $bidReserve->created_at]
                    ])
                    ->update(array('order_id' => $order->id));
            });

            return back()
                ->setStatusCode(201);

        } catch (PaymentFailedException $e) {

            return back()
                ->setStatusCode(422);
        }
    }
Snapey's avatar
Snapey
Best Answer
Level 122

@trevorpan

depends what bidreserve->complete() does. That function should be attaching the order.

trevorpan's avatar

Ok, well, at least I got this far! @snapey

here's the bidreserve->complete():

// as posted...
    public function complete($paymentGateway, $stripeToken)
    {
        $charge = $paymentGateway->charge($this->bidReserveAmount(), $stripeToken);

        return Order::forBidReserves($this->job->bidReserves(), $this->userId(), $charge);
    }

I'd like the controller to say something like $order = $bidReserve->complete($this->paymentGateway, request('stripeToken'), auth()->user()->id)->updateBidReservesTable();

Would you recommend that type of approach? I'm not sure how to get those variables which are setup in the controller into this method...

// BidReserve.php
...
    public function updateBidReservesTable()
    {
        DB::transaction(function () use($order, $job, $bidReserve) {

            DB::table('bidreserves')
                ->where([
                    ['job_id', $job->id],
                    ['created_at', $bidReserve->created_at]
                ])
                ->update(array('order_id' => $order->id));
        });

    }

gave the above a shot, but it seems like the $job, $bidReserve should be available via the container somehow.

    public function complete($paymentGateway, $stripeToken)
    {
        $charge = $paymentGateway->charge($this->bidReserveAmount(), $stripeToken);
        $orderSetup = Order::forBidReserves($this->job->bidReserves(), $this->userId(), $charge);

        $order = DB::transaction(function () use($orderSetup, $this->job, $this->bidReserve) {

                    DB::table('bidreserves')
                        ->where([
                            ['job_id', $job->id],
                            ['created_at', $bidReserve->created_at]
                        ])
                        ->update(array('order_id' => $order->id));
                });

        return $order;
    }

The above isn't working but I feel it may be a bit better (overall) than the previous approach.

trevorpan's avatar

Hi @snapey , getting back to this. Got 44 tests together, but this one is not passing, still.

I've been trying to figure out the way to make the setup in the controller available in the complete() method. Can you simply add more arguments? Like so? Maybe I'm missing something.

$order = $bidReserve->complete($this->paymentGateway, request('stripeToken'), auth()->user()->id, $job, $bidReserve);

The $job->id gives this error: Trying to get property 'id' of non-object. So this seems like the job is not being passed.

    public function complete($paymentGateway, $stripeToken, $job, $bidReserve)
    {
        $charge = $paymentGateway->charge($this->bidReserveAmount(), $stripeToken);
        $orderSetup = Order::forBidReserves($this->job->bidReserves(), $this->userId(), $charge);

        $order = DB::transaction(function () use($orderSetup, $job, $bidReserve) {

                    DB::table('bidreserves')
                        ->where([
                            ['job_id', $job->id],
                            ['created_at', $bidReserve->created_at]
                        ])
                        ->update(array('order_id' => $orderSetup->id));
                });

        return $order;
    }
Snapey's avatar

Well you have a mixture of $job and $this->job Are they the same? should you be using one and not the other?

Have you checked that $job you are passing to the complete method is a model instance and not null or a collection of models?

trevorpan's avatar

ok, thank you. That's one area I'm still foggy on. $this

dd($job) in the complete() method; gives 8 so the id of the current bidder (in the local dev context). No bueno.

I realized, the complete method was missing $user!

you are amazing. Thank you again! Going to update a few of these posts which are related to this...nightmare I've been having.

Please or to participate in this conversation.