Build profile search with Laravel, Vue and Vuex

Feb 26, 2019

Launching the ability for employers to search for qualified candidates using PHP and Javascript

Today we're launching the ability for employers to find candidates for job openings in the Bay Area. The search functionality is built with Vue.js and Vuex for state management on the frontend, while the backend is powered by Laravel and MySQL.

In this tutorial we'll go through building an employer dashboard where logged in and verified employers can search through candidates that have applied and been approved to join the Employbl network.

The end result will look something like this:

Seed the database

You'll notice this example is running on my local machine. In order to put in fake candidate data the first thing we needed to do was create a Laravel Seeder:

factory(User::class, 25)->states('active')->create()->each(function ($user) {
  $user->attachTags(Tag::findOrCreate(['Javascript', 'AWS', 'React.js'], 'candidate'));
});

This seeder uses a Laravel Factory to generate 25 users with dummy text from Alice and Wonderland with random images. Then it applies the same three tags to all 25 of those candidates.

If interested in joining Employbl as a candidate click here.

For employer access, fill out this form

Key functionality for this search dashboard is being able to filter:

  • By candidate's skills using boolean AND/OR filtering

  • By candidate's work authorization status in the United States

  • For only active candidates that have been approved to join the Employbl network

Candidate skills

For skills we have a predefined list of skills that candidates can choose to add to their profile. We're going to open source the skill list soon. To view this functionality as a candidate login to your candidate dashboard and update your profile! The skills appear in the top bar and serve as tags. The AND and OR toggles are radio buttons. If the toggle is set to AND, then only include candidates that have every single skill specified. If the toggle is set to OR, then return candidates that have any of the skills specified. In the recruiting industry this sort of filtering is known as Boolean Search.

Work authorization status

Work authorization and immigration within the United States is a hot button issue. The reality is that employers often need to make choices based on the immigration status of individuals. We ask that candidates provide this information but it is not required.

Active candidates

When candidates apply to join the Employbl network we create a database record for the candidate. Once we've reviewed the application the candidate is set to active status. We only want to display active candidates that have been vetted by Employbl.

The Code

I'm definitely a big fan of sharing and giving back to the community. Below are some provided snippets to gain a better understanding of how you could implement some of the above functionality in your own application.

Rendering the search page

Blade is Laravel's default HTML templating engine. In the Blade template file we'll render a few Vue components like so, without passing in many props:

@extends('layouts.app')

@section('content')
    <div class="row mt-5">
        <employer-search-sidebar></employer-search-sidebar>

        <div class="col-8">
            <employer-search-tags tags="{{ $allTags }}"></employer-search-tags>

            <candidate-results></candidate-results>
        </div>
    </div>
    @include('partials.footer')
@endsection

Full disclosure: please don't hack me.

On the Laravel side we have one route that renders the page. This page is protected by middleware to make sure only logged in and verified employers see the candidate search page.

public function search(Request $request) {

    $employer = Auth::guard('employer')->user();

    $allTags = Tag::getWithType('candidate')->map(function ($tag) {
        return $tag->only(['name']);
    })->pluck('name');

    $users = User::where('summary', '!=', '')
        ->orWhereNotNull('summary')
        ->active()
        ->with('tags')
        ->get();

    return view('employers.search', compact(
        'employer',
        'users',
        'allTags'
    ));
}

In this controller method we grab the logged in employer, and then get all of the possible tags that candidates can choose from. A possible improvement on this going forward would be to only select tags that candidates have picked and applied to themselves. As this is implemented right now, many tags will show up that do not have any associated candidates.

After we get all of the available tags, fetch users that have written summary's and have been approved by the Employbl team. We're using Spatie's laravel-tags package for the database tagging functionality.

Performing the search

Once the page is rendered we need to update the candidate results based on user input. Since we have to communicate across Vue.js components I elected to use Vuex for state management.

The search is triggered when the employer clicks the search button. That will trigger a Vuex action to send an API call to the backend. Once that promise is resolved, we'll do a Vuex mutation to update the results. The results will then be updated to any Vue component that looks at that state in the store. It sounds more complicated than it is.

First, the employer clicks the button, that will dispatch an Action:

Note we're using vue-multiselect for the tagging and it's pretty awesome.

<template>
  <div class="row mb-5">
    <div class="col-10">
      <multiselect placeholder="Search candidates by tag"
                   v-model="searchTags"
                   :options="allTags"
                   :taggable="true"
                   :multiple="true"></multiselect>
      <div class="form-control">
        <input type="radio" value="AND" v-model="andor">
        <label>AND</label>
        <input type="radio" value="OR" v-model="andor">
        <label>OR</label>
      </div>
    </div>
    <div class="col-2">
      <div @click="updateCandidates" class="btn btn-lg btn-primary">Search 🔍</div>
    </div>
  </div>
</template>
<script>
  import Multiselect from 'vue-multiselect';

  export default {
    props: {
      'tags': {
        required: true,
        default() {
          return '';
        }
      }
    },
    components: {
      Multiselect,
    },
    data() {
      const allTags = JSON.parse(this.tags);

      return {
        allTags,
        searchTags: [],
        andor: 'OR',
      };
    },
    methods: {
      updateCandidates() {
        this.$store.dispatch('getCandidateList', {
          tags: this.searchTags,
          filters: this.$store.state.candidateSearchFilters,
          andor: this.andor,
        });
      }
    }
  }
</script>

The data from the backend gets passed in to our prop as a string so we need to call JSON.parse() on it. this.$store is added by Vuex and has the dispatch method.

The information we need for filtering the candidates we pass in as a second argument to the dispatch method. The AND and OR filtering is normal Vue.js and almost taken directly from the docs.

From there we have an Action in the Vuex Store to send the request to the backend:

async getCandidateList({ commit }, query) {
  const tags = (_.get(query, 'tags')) ? query.tags.toString() : null;

  const workAuthFilters = _.get(query, 'filters.work_auth');

  const activeWorkAuthFilters = _.reduce(workAuthFilters, (result, value, key) => {
    return (value) ? result + key + ',' : result;
  }, '');

  const qs = querystring.stringify({
    tags,
    work_auth: activeWorkAuthFilters,
    andor: _.get(query, 'andor'),
  });

  axios.get('/api/candidates?' + qs)
    .then((response) => {
      commit('SET_CANDIDATES', response.data);
    });
}

This makes a GET request for the data that we need. The npm querystring package formats our arguments into a URI encoded string before sending the request with Axios. We only want to add the filter parameters for the boxes that are checked on the left hand side of the screen.

The Vuex mutation is the most straightforward part. After we get the data update the value to have the new data:

mutations: {
  SET_CANDIDATES(state, candidates) {
    state.candidates = candidates;
  }
},

The final part that needs explaining is what does the endpoint look like to get the new data? So glad you asked.

When we search we need to determine if AND or OR is selected, what Work Authorization statuses are applicable and what tags candidates should have.

/**
 * API route for logged in Employers to search for Candidates
 *
 * @param Request $request
 * @return \Illuminate\Http\JsonResponse
 */
public function search(Request $request)
{
    $andor = $request->query('andor');

    $searchTags = array_filter(explode(',', $request->query('tags')));

    $workAuthFilters = array_filter(explode(',', $request->query('work_auth')));

    // if no tags are specified, apply filters
    if(!$searchTags) {
        $candidates = User::whereIn('work_authorization', $workAuthFilters)
            ->active()
            ->with('tags')
            ->get();

        return response()->json($candidates);
    }

    // OR filter: inclusive. candidates that have any matching tag
    if($andor == 'OR') {
        $candidates = User::withAnyTags($searchTags, 'candidate')
            ->active()
            ->whereIn('work_authorization', $workAuthFilters)
            ->with('tags')
            ->get();

        return response()->json($candidates);

    // AND filter: exclusive. candidates must have all matching tag
    } else if($andor == 'AND') {
        $candidates = User::withAllTags($searchTags, 'candidate')
            ->active()
            ->whereIn('work_authorization', $workAuthFilters)
            ->with('tags')
            ->get();

        return response()->json($candidates);
    }
}

If no tags are specified we want to return all candidates, even if the candidate has not added tags to their profile. From there we determine if AND or OR is selected and then use methods from Spatie's tagging package to determine if we need candidates to have ALL of the tags or just greater than one.

Have you built anything similar? Know ways the code can be improved? Feel free to leave a comment below or send a message on twitter.

Let Bay Area companies find you

Join candidate network