diceline-chartmagnifiermouse-upquestion-marktwitter-whiteTwitter_Logo_Blue

Today I Learned

How to trigger model events on pivot tables in Laravel

Suppose we have an app where we have a couple of tasks and users and we want to be able to assign users to tasks.

But what if at the same time we want to know who assigned each user to each task? We can easily achieve this by taking advantage of Laravel's model events - for pivot tables.

We would need 3 tables: one for tasks, one for users and one for the assigning users to tasks:

Schema::create('tasks', function (Blueprint $table) {
    $table->id();
    $table->string("title");
    ...
    $table->timestamps();
});

Schema::create('users', function (Blueprint $table) {
    $table->id();
    $table->string("username");
    ...
    $table->timestamps();
});

Schema::create('task_user', function (Blueprint $table) {
    $table->id();
    $table->foreignId("assignee_id")->constrained("users");
    $table->foreignId("skill_id")->constrained();
    $table->foreignId("user_id")->constrained();
    $table->timestamps();
});

1. Create the corresponding TaskUser pivot model.

 php artisan make:model TaskUser --pivot

2. Add your event handlers.

class TaskUser extends Pivot
{
    protected static function booted()
    {
        static::creating(function ($pivot_model) {
            // your implementation here
        });
        ...
    }
}

3. Let Laravel know which model it should use for the pivot table.

This is the key - without this step the callback described above will not be executed!

//in the User model
public function tasks()
{
    return $this->belongsToMany(Task::class)
      ->using(TaskUser::class);
}

Now, you can do:

$user->tasks()->attach($tasks);

That's it! Your event callbacks or corresponding observer methods should now be executed.

File validation in vueJS with vee-validate

As we know, "v-model" does not work on an input that has a type of "file". So to validate a file is a little bit tricky.

For this validation, we will use vee-validate -> https://vee-validate.logaretm.com/v4/

To do this, you need to add a method that triggers the "on change" event on your input and to add on the "ValidationProvider" a ref.

import { ValidationProvider } from 'vee-validate/dist/vee-validate.full.esm'

<ValidationProvider ref="provider" rules="required">
 <input
 ref="file"
 type="file"
 @change="onFileAdd"
 />
</ValidationProvider>

Also you need to go and create the "onFileAdd" method , which will contain the following code:

export default {
 data() {
  return {
    fileData: '',
  }
 }

 methods: {
  async onFileAdd(e){
   const { valid } = await this.$refs.provider.validate(e)
   if (valid) {
    const file = this.$refs.file.files[0]
    this.fileData = file
   }
  }
 }
}

The "fileData" in the data object is the file that will be sent when the form is submitted.

Commit your changes. That's all.

JavaScript event processing and bubbling

JavaScript event listeners and handlers can have some unnatural behaviour if we are not aware of how things work behind the hood. Let's understand what these unexpected behaviours could be and why things actually happen as they happen.

Let's pretend that we have the following DOM structure with 3 divs: grandparent, parent and child.

<div class="grandparent"></div>
  <div class="parent">
    <div class="child"></div>
  </div>
</div>

All 3 divs have their custom event handlers, like follows:

grandparent.addEventListener('click', () =>  console.log('Grandparent'));
parent.addEventListener('click', () =>  console.log('Parent'));
child.addEventListener('click', () =>  console.log('Child'));

If we click on the inner div (the child div), we would expect to call the child div's event handler, but actually we will see in the console that all 3 event handlers have been called, in the following order:

Child
Parent
Grandparent

Let's understand why this happens. Mainly, there are 3 phases in processing JavaScript events:

  1. Capturing phase
  • the DOM tree is parsed downwards until the caller element is found. Throughout its way, if there are nodes that explicitly require event execution in the capturing phase, their event handlers will be executed right away.
  1. Targeting phase
  • the caller element that triggered the event is being targeted
  1. Bubbling phase
  • the DOM tree is now parsed upwards until finding the document element, starting from the targeted/caller element. If on the way up we find elements with event handlers, they will be automatically called.

If we want to stop this behaviour of calling parent elements event handlers, we can use event.stopPropagation() on our last event handler that we want to be executed.

Git doesn't recognize renamed files on a case insensitive file system

I didn't understand why git did not recognize the renamed files, also deployment build was failing due to these changes.

My initial file name was "Advanced-Menu", I changed the file name, to "advanced-menu" and committed the changes.

The actual problem is that the macOS file system is case insensitive, therefore if you rename a file on macOS, changing only the case, git will not see the changes.

In order to fix it you have to set your git repository to be case insensitive by issuing:

git config core.ignorecase false

and rename the file using git mv:

git mv Advanced-Menu advanced-menu

Commit your changes. That's all.

You could also set this change globally to prevent future issues:

git config --global  core.ignorecase false

Axios Request and Response Interceptors

Axios interceptors come in handy when we need to track, register, or work with:

  1. Requests before leaving
  2. Responses before arriving
  3. Both 1 and 2

Thanks to interceptors, we can pass our own handlers/callbacks for the following cases:

  1. Before launching a request
  2. Catching an error at HTTP request launch
  3. Arrival of a response
  4. Catching an error at response arrival

This is an example of how we can use them:

// handlers for the request launch and for catching request launch error
axios.interceptors.request.use(
   (config) => {
      console.log('We are now preparing to launch the request!')
      return config;
   },
   (error) => Promise.reject(error),
);

// handlers for response interceptor and error response interceptor
axios.interceptors.response.use(
     (response) => {
        console.log('We received the response!');
        return response;
     },
     (error) => {
       if (error.response.status === 403) 
         return Promise.reject(error);
     }
},

Each Axios Instance can have its custom configuration and request and response interceptors. This can be very useful when the architecture of our application enforces/allows each of our services to use their own Axios Instance. This way, each service can have an Axios Instance with custom configuration and custom request/response interceptors.

This is how an Axios Instance factory utility function could look like in TypeScript:

export const createAxiosWithInterceptors = (
  requestConfig: AxiosRequestConfig = {},
  requestInterceptorHandlers: Partial<RequestInterceptorHandlers> = DefaultRequestInterceptor,
  responseInterceptorHandlers: Partial<ResponseInterceptorHandlers> = DefaultResponseInterceptor,
) => {
  const axiosInstance = axios.create(requestConfig);

  axiosInstance.interceptors.request.use(
    requestInterceptorHandlers.requestConfigHandler,
    requestInterceptorHandlers.requestErrorHandler,
  );

  axiosInstance.interceptors.response.use(
    responseInterceptorHandlers.responseHandler,
    responseInterceptorHandlers.responseErrorHandler,
  );

  return axiosInstance;
};

where we can define our types and default interceptors as follows:

type RequestConfigHandler = (config: AxiosRequestConfig) => AxiosRequestConfig;
type RequestErrorHandler = (error: AxiosError) => Promise<AxiosError>;
type RequestInterceptorHandlers = {
  requestConfigHandler: RequestConfigHandler;
  requestErrorHandler: RequestErrorHandler;
};

type ResponseHandler = (response: AxiosResponse) => AxiosResponse;
type ResponseErrorHandler = (error: AxiosError) => Promise<AxiosError>;

type ResponseInterceptorHandlers = {
  responseHandler: ResponseHandler;
  responseErrorHandler: ResponseErrorHandler;
};

const DefaultResponseInterceptor = {
  responseHandler: (response: AxiosResponse) => response,
  responseErrorHandler: (error: AxiosError) => Promise.reject(error),
};

const DefaultRequestInterceptor = {
  requestConfigHandler: (config: AxiosRequestConfig) => config,
  requestErrorHandler: (error: AxiosError) => Promise.reject(error),
};

In each of our services we can then use our factory method as follows:

// axios instance with custom request config received through constructor, custom request interceptor and default response interceptor
protected http: AxiosInstance = createAxiosWithInterceptors(
    this.requestConfig,
    {
      requestConfigHandler: (config) => {
        console.log();
        return config;
      },
    },
);

How to add a script tag inside a Laravel Blade template

Sometimes you may need to add a little bit of javascript inside a Laravel Blade template. The proper way to do that is to use Laravel's @stack directive:

First, you need to add a @stack on the parent page or the layout:

<script src="{{ asset('js/app.js') }}"></script>
@stack('other-scripts')

Then you need to @push the respective script on the page you need it on.

@push('other-scripts')
<script>
  console.log('do something in js')
</script>
@endpush

How to order an ACF repeater field in descending order by key

The problem:

<?php if ( have_rows( 'repeater_field' ) ) while ( have_rows( 'repeater_field' ) ) : the_row(); ?>
    <?php
        $name = get_sub_field( 'name' );
        $age = get_sub_field( 'age' );
    ?>
	
    <p><?php echo $name; ?></p>
    <p><?php echo $age; ?></p>
<?php endwhile; ?>

This only gets you the normal, ascending order for those repeater rows. But what if you need to reverse the order of those rows, making first the last and vice-versa?

The Advanced Custom Fields documentation gives an example, but that didn't work at the time of this writing. They instruct you to use the get_field() function for the main repeater field, but for some reason that returns null on a repeater field.

What I've found to work was to use the get_sub_field() to get the field and the krsort() php method which sorts an array by key in descending order.

<?php
    $repeater = get_sub_field('repeater_field');
    krsort($repeater);
?>
<?php foreach ($repeater as $row): ?>
    <p><?php echo $row['name']; ?></p>
    <p><?php echo $row['age']; ?></p>
<?php endforeach; ?>

One thing to note is that with this method you need to change the way you access those variables.

How use Laravel's Bootable Eloquent Traits

Given the following bootable trait:

trait WithCreator
{
    //no need to define a 'boot' method
    public static function bootWithCreator()
    {
        self::observe(CreatorObserver::class);
    }
}
class CreatorObserver
{
    public function creating($model)
    {
        $model->creator()->associate(auth()->user());
    }
}

We can use it to register the observer without colliding with existing boot methods inside the model.

class Comment extends Model
{
    use HasFactory;
    use WithCreator;
    
    public static function boot()
    {
        ...
    }
}