Passed
Push — task/application-handle-step-s... ( 5f39f9...d6d197 )
by Yonathan
05:39
created

addNestedValidatorRules()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 12
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 6
c 1
b 0
f 0
dl 0
loc 12
rs 10
cc 1
nc 1
nop 3
1
<?php
2
3
namespace App\Services\Validation;
4
5
use App\Models\ExperienceSkill;
6
use App\Models\JobApplication;
7
use App\Models\Lookup\CriteriaType;
8
use App\Models\Lookup\LanguageRequirement;
9
use App\Services\Validation\JobApplicationAnswerValidator;
10
use App\Services\Validation\Rules\ContainsArrayWithAttributeRule;
11
use App\Services\Validation\Rules\ContainsObjectWithAttributeRule;
12
use Illuminate\Database\Eloquent\Builder;
13
use Illuminate\Support\Facades\Validator;
14
15
class ApplicationTimelineValidator
16
{
17
    /**
18
     * @var mixed $backendRules Rule array shared by validator()
19
     * and detailedValidationErrors(). Requires a Job Poster ID,
20
     * an Application Status ID, and an Applicant ID
21
     */
22
    public $backendRules = [
23
        'job_poster_id' => 'required',
24
        'application_status_id' => 'required',
25
        'applicant_id' => 'required',
26
    ];
27
28
    /* Step 1 ------------------------------------------------------------------------------------------------------- */
29
30
    /**
31
     * Validation rules for the Basic Info step.
32
     *
33
     * @return mixed[]
34
     */
35
    protected function basicInfoSimpleRules()
36
    {
37
        // Field 'language_test_confirmed' needs to be validated conditionally.
38
        return [
39
            'language_requirement_confirmed' => ['required', 'boolean', 'accepted'],
40
            'citizenship_declaration_id' => ['required', 'exists:citizenship_declarations,id'],
41
            'veteran_status_id' => ['required', 'exists:veteran_statuses,id'],
42
            'preferred_language_id' => ['required', 'exists:preferred_languages,id'],
43
            'education_requirement_confirmed' => ['required', 'boolean', 'accepted'],
44
        ];
45
    }
46
47
    /**
48
     * Validator instance for the Basic Info step.
49
     *
50
     * @param JobApplication $application Job Application object.
51
     *
52
     * @return \Illuminate\Contracts\Validation\Validator
53
     */
54
    public function basicsValidator(JobApplication $application)
55
    {
56
        $application->loadMissing('job_poster');
57
58
        // Check to see if this application is related to a Job Poster with
59
        // a bilingual requirement.
60
        $langRequirement = $application->job_poster->language_requirement_id;
61
        $langTestRequirement = LanguageRequirement::where('name', 'bilingual_intermediate')
62
            ->orWhere('name', 'bilingual_advanced')
63
            ->pluck('id');
64
65
        $validator = Validator::make($application->toArray(), $this->basicInfoSimpleRules());
66
67
        // Conditionally check for 'language_test_confirmed' if the
68
        // closure returns true.
69
        $validator->sometimes(
70
            'language_test_confirmed',
71
            'required|boolean|accepted',
72
            function ($input) use ($langRequirement, $langTestRequirement) {
73
                return in_array($langRequirement, $langTestRequirement->toArray());
74
            }
75
        );
76
77
        return $validator;
78
    }
79
80
    /**
81
     * Helper function to return completeness for the Basic Info step.
82
     *
83
     * @param JobApplication $application Job Application object.
84
     *
85
     * @return boolean
86
     */
87
    public function basicsComplete(JobApplication $application)
88
    {
89
        $validator = $this->basicsValidator($application);
90
        return $validator->passes();
91
    }
92
93
    /* End Step 1 --------------------------------------------------------------------------------------------------- */
94
95
    /* Step 2 ------------------------------------------------------------------------------------------------------- */
96
97
    /**
98
     * Validator instance for the Experience step.
99
     *
100
     * @param JobApplication $application Job Application object.
101
     *
102
     * @return \Illuminate\Contracts\Validation\Validator
103
     */
104
    public function experienceValidator(JobApplication $application)
105
    {
106
        $application->loadMissing('job_poster', 'job_poster.criteria');
107
108
        $essentialCriteriaType = CriteriaType::where('name', 'essential')->first()->id;
109
110
        $jobCriteria = $application->job_poster->criteria;
111
        $requiredCriteria = $jobCriteria->filter(function ($criterion) use ($essentialCriteriaType) {
112
            return $criterion->criteria_type_id === $essentialCriteriaType;
113
        });
114
115
        $experienceSkillRules = array();
116
117
        foreach ($requiredCriteria as $criterion) {
118
            // Validate that every essential skill has a corresponding experience.
119
            $experienceSkillRules[] = [
120
                'skill_id' => new ContainsArrayWithAttributeRule('skill_id', $criterion->skill_id)
121
            ];
122
        }
123
124
        // Get all Experiences belonging to the current application, that are assigned to required Criteria.
125
        $experiences = ExperienceSkill::whereHasMorph(
126
            'experience',
127
            '*',
128
            function (Builder $query) use ($application): void {
129
                $query->where([
130
                    ['experienceable_type', 'application'],
131
                    ['experienceable_id', $application->id]
132
                ]);
133
            }
134
        )
135
        ->whereIn('skill_id', $requiredCriteria->pluck('skill_id')->all())
136
        ->get();
137
138
        $validator = Validator::make($experiences->toArray(), $experienceSkillRules);
139
140
        return $validator;
141
    }
142
143
    /**
144
     * Helper function to return completeness for the Experience step.
145
     *
146
     * @param JobApplication $application Job Application object.
147
     *
148
     * @return boolean
149
     */
150
    public function experienceComplete(JobApplication $application)
151
    {
152
        $validator = $this->experienceValidator($application);
153
        return $validator->passes();
154
    }
155
156
    /* End Step 2 --------------------------------------------------------------------------------------------------- */
157
158
    /* Step 3 ------------------------------------------------------------------------------------------------------- */
159
160
    /**
161
     * Validator instance for the Skills step.
162
     *
163
     * @param JobApplication $application Job Application object.
164
     *
165
     * @return \Illuminate\Contracts\Validation\Validator
166
     */
167
    public function skillsValidator(JobApplication $application)
168
    {
169
        $application->loadMissing('job_poster', 'job_poster.criteria');
170
171
        $essentialCriteriaType = CriteriaType::where('name', 'essential')->first()->id;
172
173
        $jobCriteria = $application->job_poster->criteria;
174
        $requiredCriteria = $jobCriteria->filter(function ($criterion) use ($essentialCriteriaType) {
175
            return $criterion->criteria_type_id === $essentialCriteriaType;
176
        });
177
178
        // Validate that every experience has a justification.
179
        $experienceSkillRules = array('*.justification' => 'required|max:100');
180
181
        foreach ($requiredCriteria as $criterion) {
182
            // Validate that every essential skill has a corresponding experience.
183
            $experienceSkillRules[] = [
184
                'skill_id' => new ContainsArrayWithAttributeRule('skill_id', $criterion->skill_id),
185
            ];
186
        }
187
188
        // Get all Experiences belonging to the current application, that are assigned to required Criteria.
189
        $experiences = ExperienceSkill::whereHasMorph(
190
            'experience',
191
            '*',
192
            function (Builder $query) use ($application): void {
193
                $query->where([
194
                    ['experienceable_type', 'application'],
195
                    ['experienceable_id', $application->id]
196
                ]);
197
            }
198
        )
199
        ->whereIn('skill_id', $requiredCriteria->pluck('skill_id')->all())
200
        ->get();
201
202
        $validator = Validator::make($experiences->toArray(), $experienceSkillRules);
203
204
        return $validator;
205
    }
206
207
    /**
208
     * Helper function to return completeness for the Skills step.
209
     *
210
     * @param JobApplication $application Job Application object.
211
     *
212
     * @return boolean
213
     */
214
    public function skillsComplete(JobApplication $application)
215
    {
216
        $validator = $this->skillsValidator($application);
217
        return $validator->passes();
218
    }
219
220
    /* End Step 3 --------------------------------------------------------------------------------------------------- */
221
222
    /* Step 4 ------------------------------------------------------------------------------------------------------- */
223
224
    /**
225
     * Validator instance for the Fit step.
226
     *
227
     * @param JobApplication $application Job Application object.
228
     *
229
     * @return \Illuminate\Contracts\Validation\Validator
230
     */
231
    public function fitValidator(JobApplication $application)
232
    {
233
        // Load application answers so they are included in application->toArray().
234
        $application->load('job_application_answers');
235
236
        // Start with Answer rules, that ensure each answer is complete.
237
        $answerValidator = new JobApplicationAnswerValidator($application);
238
239
        $rules = $this->addNestedValidatorRules(
240
            'job_application_answers.*',
241
            $answerValidator->rules(),
242
            []
243
        );
244
245
        // Validate that each question has been answered.
246
        $jobPosterQuestionRules = [];
247
        foreach ($application->job_poster->job_poster_questions as $question) {
248
            $jobPosterQuestionRules[] = new ContainsObjectWithAttributeRule('job_poster_question_id', $question->id);
249
        }
250
        $rules['job_application_answers'] = $jobPosterQuestionRules;
251
252
        $validator = Validator::make($application->toArray(), $rules);
253
254
        return $validator;
255
    }
256
257
    /**
258
     * Helper function to return completeness for the Fit step.
259
     *
260
     * @param JobApplication $application Job Application object.
261
     *
262
     * @return boolean
263
     */
264
    public function fitComplete(JobApplication $application)
265
    {
266
        $validator = $this->fitValidator($application);
267
        return $validator->passes();
268
    }
269
270
    /* End Step 4 --------------------------------------------------------------------------------------------------- */
271
272
    /* Step 5 ------------------------------------------------------------------------------------------------------- */
273
274
    /**
275
     * Validator instance for the entire Application process.
276
     *
277
     * @param JobApplication $application Job Application object.
278
     *
279
     * @return \Illuminate\Contracts\Validation\Validator
280
     */
281
    public function validator(JobApplication $application)
282
    {
283
        $data = $application->toArray();
284
285
        $rules = $this->backendRules;
286
287
        // Combining and simplifying error messages.
288
        $rules = array_merge(
289
            $rules,
290
            ['timeline_step_1' => 'required|boolean|accepted'],
291
            ['timeline_step_2' => 'required|boolean|accepted'],
292
            ['timeline_step_3' => 'required|boolean|accepted'],
293
            ['timeline_step_4' => 'required|boolean|accepted'],
294
            ['timeline_step_5' => 'required|boolean|accepted']
295
        );
296
        $data = array_merge(
297
            $data,
298
            ['timeline_step_1' => $this->basicsComplete($application)],
299
            ['timeline_step_2' => $this->experienceComplete($application)],
300
            ['timeline_step_3' => $this->skillsComplete($application)],
301
            ['timeline_step_4' => $this->fitComplete($application)],
302
            ['timeline_step_5' => $this->affirmationComplete($application)]
303
        );
304
305
        // Validate basic data is filled in.
306
        return Validator::make($data, $rules);
307
    }
308
309
    public function validate(JobApplication $application)
310
    {
311
        $this->validator($application)->validate();
312
    }
313
314
    public function validateComplete(JobApplication $application)
315
    {
316
        $validator = $this->validator($application);
317
        return $validator->passes();
318
    }
319
320
    /* End Step 5 --------------------------------------------------------------------------------------------------- */
321
322
    /* Step 6 ------------------------------------------------------------------------------------------------------- */
323
324
    /**
325
     * Validator instance for the Final Submit step.
326
     *
327
     * @param JobApplication $application Job Application object.
328
     *
329
     * @return \Illuminate\Contracts\Validation\Validator
330
     */
331
    public function affirmationValidator(JobApplication $application)
332
    {
333
        return Validator::make($application->toArray(), [
334
            'submission_signature' => [
335
                'required',
336
                'string',
337
                'max:191',
338
            ],
339
            'submission_date' => [
340
                'required',
341
                'string',
342
                'max:191',
343
            ]
344
        ]);
345
    }
346
347
    /**
348
     * Helper function to return completeness for the Final Submit step.
349
     *
350
     * @param JobApplication $application Job Application object.
351
     *
352
     * @return boolean
353
     */
354
    public function affirmationComplete(JobApplication $application)
355
    {
356
        return $this->affirmationValidator($application)->passes();
357
    }
358
359
    /* End Step 6 --------------------------------------------------------------------------------------------------- */
360
361
    /* Helpers ------------------------------------------------------------------------------------------------------ */
362
363
    /**
364
     * Helper function to return detailed error messages for each step.
365
     *
366
     * @param JobApplication $application Job Application object.
367
     *
368
     * @return mixed
369
     */
370
    public function detailedValidatorErrors(JobApplication $application)
371
    {
372
        return array_merge(
373
            Validator::make($application->toArray(), $this->backendRules)->errors()->all(),
374
            $this->basicsValidator($application)->errors()->all(),
375
            $this->experienceValidator($application)->errors()->all(),
376
            $this->skillsValidator($application)->errors()->all(),
377
            $this->fitValidator($application)->errors()->all(),
378
            $this->affirmationValidator($application)->errors()->all()
379
        );
380
    }
381
382
    /**
383
     * Return a copy of $array, with function $fn applied to each key, but values left unchanged.
384
     *
385
     * @param function $fn    Function applied to each key.
386
     * @param mixed    $array Array to operate on.
387
     * @return array
388
     */
389
    protected function arrayMapKeys($fn, $array): array
390
    {
391
        $newArray = [];
392
        foreach ($array as $key => $value) {
393
            $newArray[$fn($key)] = $value;
394
        }
395
        return $newArray;
396
    }
397
398
    /**
399
     * Return a copy of $array, with function $fn applied to each key, but values left unchanged.
400
     *
401
     * @param string $nestedAttribute Attribute name to apply to each rule.
402
     * @param mixed  $validatorRules  Rules array that requires an attribute name to be prepended.
403
     * @param mixed  $rules           Optional existing validation rules to merge with.
404
     *
405
     * @return array
406
     */
407
    protected function addNestedValidatorRules($nestedAttribute, $validatorRules, $rules = [])
408
    {
409
        // Prepend the attribute name of each validator rule with the nested attribute name.
410
        $newRules = $this->arrayMapKeys(
411
            function ($key) use ($nestedAttribute) {
0 ignored issues
show
Bug introduced by
function(...) { /* ... */ } of type callable is incompatible with the type App\Services\Validation\function expected by parameter $fn of App\Services\Validation\...lidator::arrayMapKeys(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

411
            /** @scrutinizer ignore-type */ function ($key) use ($nestedAttribute) {
Loading history...
412
                return implode('.', [$nestedAttribute, $key]);
413
            },
414
            $validatorRules
415
        );
416
        // Merge new rules with old rules.
417
        $rules = array_merge($rules, $newRules);
418
        return $rules;
419
    }
420
    /* End Helpers -------------------------------------------------------------------------------------------------- */
421
}
422