MailEclipse   F
last analyzed

Complexity

Total Complexity 149

Size/Duplication

Total Lines 1085
Duplicated Lines 0 %

Importance

Changes 27
Bugs 10 Features 2
Metric Value
eloc 454
c 27
b 10
f 2
dl 0
loc 1085
rs 2
wmc 149

32 Methods

Rating   Name   Duplication   Size   Complexity  
A getMailables() 0 3 1
A getTemplates() 0 3 1
A previewMarkdownHtml() 0 3 1
A markdownedTemplateToView() 0 13 4
A markdownedTemplate() 0 5 1
A createTemplate() 0 44 5
B updateTemplate() 0 55 6
A getTemplatesFile() 0 13 5
A saveTemplates() 0 3 1
A getMailableTemplateData() 0 27 4
A getTemplate() 0 25 4
A previewMarkdownViewContent() 0 21 5
A deleteTemplate() 0 26 4
A getMailable() 0 3 1
A viewDataInspect() 0 24 4
A setMailableSendTestRecipient() 0 7 1
D mailablesList() 0 95 21
B loadRelations() 0 40 8
B generateMailable() 0 56 9
B handleMailableViewDataArgs() 0 66 11
A resolveMailableInstance() 0 9 2
A generateClassName() 0 29 5
A renderMailable() 0 17 3
A getMarkdownViewName() 0 13 2
A sendTest() 0 9 1
A mailable_exists() 0 7 2
A hydrateRelations() 0 41 5
A resolveFactory() 0 19 5
A getMissingParams() 0 27 5
B renderPreview() 0 44 8
B getMailableViewData() 0 45 11
A buildMailable() 0 11 3

How to fix   Complexity   

Complex Class

Complex classes like MailEclipse often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use MailEclipse, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Qoraiche\MailEclipse;
4
5
use ErrorException;
6
use Illuminate\Database\Eloquent\Factory as EloquentFactory;
7
use Illuminate\Http\JsonResponse;
8
use Illuminate\Mail\Markdown;
9
use Illuminate\Support\Collection;
10
use Illuminate\Support\Facades\Artisan;
11
use Illuminate\Support\Facades\DB;
12
use Illuminate\Support\Facades\File;
13
use Illuminate\Support\Facades\Log;
14
use Illuminate\Support\Facades\Mail;
15
use Illuminate\Support\Facades\View;
16
use Illuminate\Support\Str;
17
use Qoraiche\MailEclipse\Utils\Replacer;
18
use RecursiveDirectoryIterator;
19
use RecursiveIteratorIterator;
20
use ReeceM\Mocker\Mocked;
21
use ReflectionClass;
22
use ReflectionProperty;
23
use RegexIterator;
24
25
/**
26
 * Class MailEclipse.
27
 */
28
class MailEclipse
29
{
30
    public const VIEW_NAMESPACE = 'maileclipse';
31
32
    public const VERSION = '3.5.0';
33
34
    /**
35
     * Default type examples for being passed to reflected classes.
36
     *
37
     * @var array TYPES
38
     */
39
    public const TYPES = [
40
        'int' => 31,
41
        'string' => null,
42
        'bool' => false,
43
        'float' => 3.14159,
44
        'iterable' => null,
45
        'array' => null,
46
    ];
47
48
    public static $traversed = 0;
49
50
    /**
51
     * @return array
52
     *
53
     * @throws \ReflectionException
54
     */
55
    public static function getMailables()
56
    {
57
        return self::mailablesList();
58
    }
59
60
    /**
61
     * @param $key
62
     * @param $name
63
     * @return Collection
64
     *
65
     * @throws \ReflectionException
66
     */
67
    public static function getMailable($key, $name): Collection
68
    {
69
        return collect(self::getMailables())->where($key, $name);
70
    }
71
72
    /**
73
     * @param $templateSlug
74
     * @return bool
75
     */
76
    public static function deleteTemplate($templateSlug): bool
77
    {
78
        $template = self::getTemplates()
79
            ->where('template_slug', $templateSlug)->first();
80
81
        if ($template !== null) {
82
            self::saveTemplates(self::getTemplates()->reject(function ($value) use ($template) {
83
                return $value->template_slug === $template->template_slug;
84
            }));
85
86
            $template_view = self::VIEW_NAMESPACE.'::templates.'.$templateSlug;
87
            $template_plaintext_view = $template_view.'_plain_text';
88
89
            if (View::exists($template_view)) {
90
                unlink(View($template_view)->getPath());
91
92
                // remove plain text template version when exists
93
                if (View::exists($template_plaintext_view)) {
94
                    unlink(View($template_plaintext_view)->getPath());
95
                }
96
97
                return true;
98
            }
99
        }
100
101
        return false;
102
    }
103
104
    /**
105
     * @return string
106
     */
107
    public static function getTemplatesFile()
108
    {
109
        $file = config('maileclipse.mailables_dir').'templates.json';
110
        if (! file_exists($file)) {
111
            if (! file_exists(config('maileclipse.mailables_dir'))) {
112
                if (! mkdir($concurrentDirectory = config('maileclipse.mailables_dir')) && ! is_dir($concurrentDirectory)) {
113
                    throw new \RuntimeException(sprintf('Directory "%s" was not created', $concurrentDirectory));
114
                }
115
            }
116
            file_put_contents($file, '[]');
117
        }
118
119
        return $file;
120
    }
121
122
    /**
123
     * Save templates to templates.json file.
124
     *
125
     * @param  Collection  $templates
126
     */
127
    public static function saveTemplates(Collection $templates): void
128
    {
129
        file_put_contents(self::getTemplatesFile(), $templates->toJson());
130
    }
131
132
    /**
133
     * @param $request
134
     * @return JsonResponse|null
135
     */
136
    public static function updateTemplate($request): ?JsonResponse
137
    {
138
        $template = self::getTemplates()
139
            ->where('template_slug', $request->templateslug)->first();
140
141
        if ($template !== null) {
142
            if (! preg_match("/^[a-zA-Z0-9-_\s]+$/", $request->title)) {
143
                return response()->json([
144
                    'status' => 'failed',
145
                    'message' => 'Template name not valid',
146
                ]);
147
            }
148
149
            $templateName = Str::camel(preg_replace('/\s+/', '_', $request->title));
150
151
            if (self::getTemplates()->contains('template_slug', '=', $templateName)) {
152
                return response()->json([
153
154
                    'status' => 'failed',
155
                    'message' => 'Template name already exists',
156
157
                ]);
158
            }
159
160
            // Update
161
            $oldForm = self::getTemplates()->reject(function ($value) use ($template) {
162
                return $value->template_slug === $template->template_slug;
163
            });
164
            $newForm = array_merge($oldForm->toArray(), [array_merge((array) $template, [
165
                'template_slug' => $templateName,
166
                'template_name' => $request->title,
167
                'template_description' => $request->description,
168
            ])]);
169
170
            self::saveTemplates(collect($newForm));
171
172
            $template_view = self::VIEW_NAMESPACE.'::templates.'.$request->templateslug;
173
            $template_plaintext_view = $template_view.'_plain_text';
174
175
            if (View::exists($template_view)) {
176
                $viewPath = View($template_view)->getPath();
177
178
                rename($viewPath, dirname($viewPath)."/{$templateName}.blade.php");
179
180
                if (View::exists($template_plaintext_view)) {
181
                    $textViewPath = View($template_plaintext_view)->getPath();
182
183
                    rename($textViewPath, dirname($viewPath)."/{$templateName}_plain_text.blade.php");
184
                }
185
            }
186
187
            return response()->json([
188
                'status' => 'ok',
189
                'message' => 'Updated Successfully',
190
                'template_url' => route('viewTemplate', ['templatename' => $templateName]),
191
            ]);
192
        }
193
    }
194
195
    /**
196
     * @param $templateSlug
197
     * @return Collection|null
198
     */
199
    public static function getTemplate($templateSlug): ?Collection
200
    {
201
        $template = self::getTemplates()->where('template_slug', $templateSlug)->first();
202
203
        if ($template !== null) {
204
            $template_view = self::VIEW_NAMESPACE.'::templates.'.$template->template_slug;
205
            $template_plaintext_view = $template_view.'_plain_text';
206
207
            if (View::exists($template_view)) {
208
                $viewPath = View($template_view)->getPath();
209
                $textViewPath = View($template_plaintext_view)->getPath();
210
211
                /** @var Collection $templateData */
212
                $templateData = collect([
213
                    'template' => Replacer::toEditor(file_get_contents($viewPath)),
214
                    'plain_text' => View::exists($template_plaintext_view) ? file_get_contents($textViewPath) : '',
215
                    'slug' => $template->template_slug,
216
                    'name' => $template->template_name,
217
                    'description' => $template->template_description,
218
                    'template_type' => $template->template_type,
219
                    'template_view_name' => $template->template_view_name,
220
                    'template_skeleton' => $template->template_skeleton,
221
                ]);
222
223
                return $templateData;
224
            }
225
        }
226
    }
227
228
    /**
229
     * Get templates collection.
230
     *
231
     * @return Collection
232
     */
233
    public static function getTemplates(): Collection
234
    {
235
        return collect(json_decode(file_get_contents(self::getTemplatesFile())));
236
    }
237
238
    /**
239
     * @param $request
240
     * @return JsonResponse
241
     */
242
    public static function createTemplate($request): JsonResponse
243
    {
244
        if (! preg_match("/^[a-zA-Z0-9-_\s]+$/", $request->template_name)) {
245
            return response()->json([
246
                'status' => 'error',
247
                'message' => 'Template name not valid',
248
249
            ]);
250
        }
251
252
        $view = self::VIEW_NAMESPACE.'::templates.'.$request->template_name;
253
        $templateName = Str::camel(preg_replace('/\s+/', '_', $request->template_name));
254
255
        if (! view()->exists($view) && ! self::getTemplates()->contains('template_slug', '=', $templateName)) {
256
            self::saveTemplates(self::getTemplates()
257
                ->push([
258
                    'template_name' => $request->template_name,
259
                    'template_slug' => $templateName,
260
                    'template_description' => $request->template_description,
261
                    'template_type' => $request->template_type,
262
                    'template_view_name' => $request->template_view_name,
263
                    'template_skeleton' => $request->template_skeleton,
264
                ]));
265
266
            $dir = resource_path('views/vendor/'.self::VIEW_NAMESPACE.'/templates');
267
268
            if (! File::isDirectory($dir)) {
269
                File::makeDirectory($dir, 0755, true);
270
            }
271
272
            file_put_contents($dir."/{$templateName}.blade.php", Replacer::toBlade($request->content));
273
274
            file_put_contents($dir."/{$templateName}_plain_text.blade.php", $request->plain_text);
275
276
            return response()->json([
277
                'status' => 'ok',
278
                'message' => 'Template created<br> <small>Reloading...<small>',
279
                'template_url' => route('viewTemplate', ['templatename' => $templateName]),
280
            ]);
281
        }
282
283
        return response()->json([
284
            'status' => 'error',
285
            'message' => 'Template not created',
286
287
        ]);
288
    }
289
290
    protected static function markdownedTemplate($viewPath)
291
    {
292
        $viewContent = file_get_contents($viewPath);
293
294
        return Replacer::toEditor($viewContent);
295
    }
296
297
    /**
298
     * Markdowned template view.
299
     */
300
    public static function markdownedTemplateToView($save = true, $content = '', $viewPath = '', $template = false)
301
    {
302
        if ($template && View::exists(self::VIEW_NAMESPACE.'::templates.'.$viewPath)) {
303
            $viewPath = View(self::VIEW_NAMESPACE.'::templates.'.$viewPath)->getPath();
304
        }
305
306
        $replaced = Replacer::toBlade($content);
307
308
        if (! $save) {
309
            return $replaced;
310
        }
311
312
        return file_put_contents($viewPath, $replaced) !== false;
313
    }
314
315
    /**
316
     * @param $simpleview
317
     * @param $content
318
     * @param $viewName
319
     * @param  bool  $template
320
     * @param  null  $namespace
0 ignored issues
show
Documentation Bug introduced by
Are you sure the doc-type for parameter $namespace is correct as it would always require null to be passed?
Loading history...
321
     * @return bool|string|void
322
     *
323
     * @throws \ReflectionException
324
     */
325
    public static function previewMarkdownViewContent($simpleview, $content, $viewName, $template = false, $namespace = null)
326
    {
327
        $previewtoset = self::markdownedTemplateToView(false, $content);
328
        $dir = dirname(__FILE__, 2).'/resources/views/draft';
329
        $viewName = $template ? $viewName.'_template' : $viewName;
330
331
        if (file_exists($dir)) {
332
            file_put_contents($dir."/{$viewName}.blade.php", $previewtoset);
333
334
            if ($template) {
335
                $instance = null;
336
            } elseif (self::handleMailableViewDataArgs($namespace) !== null) {
337
                $instance = self::handleMailableViewDataArgs($namespace);
338
            } else {
339
                $instance = new $namespace;
340
            }
341
342
            return self::renderPreview($simpleview, self::VIEW_NAMESPACE.'::draft.'.$viewName, $template, $instance);
0 ignored issues
show
Bug introduced by
It seems like $instance can also be of type object; however, parameter $instance of Qoraiche\MailEclipse\MailEclipse::renderPreview() does only seem to accept null, maybe add an additional type check? ( Ignorable by Annotation )

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

342
            return self::renderPreview($simpleview, self::VIEW_NAMESPACE.'::draft.'.$viewName, $template, /** @scrutinizer ignore-type */ $instance);
Loading history...
343
        }
344
345
        return false;
346
    }
347
348
    /**
349
     * @param $instance
350
     * @param $view
351
     * @return string|void
352
     */
353
    public static function previewMarkdownHtml($instance, $view)
354
    {
355
        return self::renderPreview($instance, $view);
356
    }
357
358
    /**
359
     * @param $mailableName
360
     * @return array|bool
361
     */
362
    public static function getMailableTemplateData($mailableName)
363
    {
364
        $mailable = self::getMailable('name', $mailableName);
365
366
        if ($mailable->isEmpty()) {
367
            return false;
368
        }
369
370
        $templateData = collect($mailable->first())->only(['markdown', 'view_path', 'text_view_path', 'text_view', 'view_data', 'data', 'namespace'])->all();
371
372
        $templateExists = $templateData['view_path'] !== null;
373
        $textTemplateExists = $templateData['text_view_path'] !== null;
374
375
        if ($templateExists) {
376
            $viewPathParams = collect($templateData)->union([
377
378
                'text_template' => $textTemplateExists ? file_get_contents($templateData['text_view_path']) : null,
379
                'template' => file_get_contents($templateData['view_path']),
380
                'markdowned_template' => self::markdownedTemplate($templateData['view_path']),
381
                'template_name' => $templateData['markdown'] ?? $templateData['data']->view,
382
                'is_markdown' => $templateData['markdown'] !== null,
383
            ])->all();
384
385
            return $viewPathParams;
386
        }
387
388
        return $templateData;
389
    }
390
391
    /**
392
     * @param  null  $request
0 ignored issues
show
Documentation Bug introduced by
Are you sure the doc-type for parameter $request is correct as it would always require null to be passed?
Loading history...
393
     * @return JsonResponse
394
     */
395
    public static function generateMailable($request = null): JsonResponse
396
    {
397
        $name = self::generateClassName($request->input('name'));
0 ignored issues
show
Bug introduced by
The method input() does not exist on null. ( Ignorable by Annotation )

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

397
        $name = self::generateClassName($request->/** @scrutinizer ignore-call */ input('name'));

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
398
        $defaultDirectory = 'Mail';
399
400
        $mailableDir = config('maileclipse.mailables_dir');
401
        $customPath = substr($mailableDir, strpos($mailableDir, $defaultDirectory) + strlen($defaultDirectory) + 1);
402
403
        if ($name === false) {
404
            return response()->json([
405
                'status' => 'error',
406
                'message' => 'Wrong name format.',
407
            ]);
408
        }
409
410
        if (! $request->has('force') && ! self::getMailable('name', $name)->isEmpty()) {
411
            return response()->json([
412
                'status' => 'error',
413
                'message' => 'This mailable name already exists. names should be unique! to override it, enable "force" option.',
414
            ]);
415
        }
416
417
        if (strtolower($name) === 'mailable') {
418
            return response()->json([
419
                'status' => 'error',
420
                'message' => 'You cannot use "mailable" as a mailable name',
421
            ]);
422
        }
423
424
        $name = $customPath ? $customPath.'/'.$name : $name;
425
426
        $params = collect([
427
            'name' => $name,
428
        ]);
429
430
        if ($request->input('markdown')) {
431
            $params->put('--markdown', $request->markdown);
432
        }
433
434
        if ($request->has('force')) {
435
            $params->put('--force', true);
436
        }
437
438
        $exitCode = Artisan::call('make:mail', $params->all());
439
440
        if ($exitCode > -1) {
441
            return response()->json([
442
                'status' => 'ok',
443
                'message' => 'Mailable Created<br> <small>Reloading...<small>',
444
            ]);
445
        }
446
447
        return response()->json([
448
449
            'status' => 'error',
450
            'message' => 'mailable not created successfully',
451
452
        ]);
453
    }
454
455
    /**
456
     * Get Mailables list.
457
     *
458
     * @return array
459
     *
460
     * @throws \ReflectionException
461
     */
462
    protected static function mailablesList()
463
    {
464
        $fqcns = [];
465
466
        if (! file_exists(config('maileclipse.mailables_dir'))) {
467
            return;
468
        } else {
469
            $allFiles = new RecursiveIteratorIterator(new RecursiveDirectoryIterator(config('maileclipse.mailables_dir')));
470
            $phpFiles = new RegexIterator($allFiles, '/\.php$/');
471
            $i = 0;
472
473
            foreach ($phpFiles as $phpFile) {
474
                $i++;
475
                $content = file_get_contents($phpFile->getRealPath());
476
                $tokens = token_get_all($content);
477
                $namespace = '';
478
                for ($index = 0; isset($tokens[$index]); $index++) {
479
                    if (! isset($tokens[$index][0])) {
480
                        continue;
481
                    }
482
                    if (T_NAMESPACE === $tokens[$index][0]) {
483
                        $index += 2; // Skip namespace keyword and whitespace
484
                        while (isset($tokens[$index]) && is_array($tokens[$index])) {
485
                            $namespace .= $tokens[$index++][1];
486
                        }
487
                    }
488
                    if (T_CLASS === $tokens[$index][0] && T_WHITESPACE === $tokens[$index + 1][0] && T_STRING === $tokens[$index + 2][0]) {
489
                        $index += 2; // Skip class keyword and whitespace
490
491
                        [$name, $extension] = explode('.', $phpFile->getFilename());
492
493
                        $mailableClass = $namespace.'\\'.$tokens[$index][1];
494
495
                        if (! self::mailable_exists($mailableClass)) {
496
                            continue;
497
                        }
498
499
                        $reflector = new ReflectionClass($mailableClass);
500
501
                        if ($reflector->isAbstract()) {
502
                            continue;
503
                        }
504
505
                        $mailable_data = self::buildMailable($mailableClass);
506
507
                        if (! is_null(self::handleMailableViewDataArgs($mailableClass))) {
508
                            $mailable_view_data = self::getMailableViewData(self::handleMailableViewDataArgs($mailableClass), $mailable_data);
509
                        } else {
510
                            $mailable_view_data = self::getMailableViewData(new $mailableClass, $mailable_data);
511
                        }
512
513
                        $fqcns[$i]['data'] = $mailable_data;
514
                        $fqcns[$i]['markdown'] = self::getMarkdownViewName($mailable_data);
515
                        $fqcns[$i]['name'] = $name;
516
                        $fqcns[$i]['namespace'] = $mailableClass;
517
                        $fqcns[$i]['filename'] = $phpFile->getFilename();
518
                        $fqcns[$i]['modified'] = $phpFile->getCTime();
519
                        $fqcns[$i]['viewed'] = $phpFile->getATime();
520
                        $fqcns[$i]['view_data'] = $mailable_view_data;
521
                        // $fqcns[$i]['view_data'] = [];
522
                        $fqcns[$i]['path_name'] = $phpFile->getPathname();
523
                        $fqcns[$i]['readable'] = $phpFile->isReadable();
524
                        $fqcns[$i]['writable'] = $phpFile->isWritable();
525
                        $fqcns[$i]['view_path'] = null;
526
                        $fqcns[$i]['text_view_path'] = null;
527
528
                        if (! is_null($fqcns[$i]['markdown']) && View::exists($fqcns[$i]['markdown'])) {
529
                            $fqcns[$i]['view_path'] = View($fqcns[$i]['markdown'])->getPath();
530
                        }
531
532
                        if (! is_null($fqcns[$i]['data'])) {
533
                            if (! is_null($fqcns[$i]['data']->view) && View::exists($fqcns[$i]['data']->view)) {
534
                                $fqcns[$i]['view_path'] = View($fqcns[$i]['data']->view)->getPath();
535
                            }
536
537
                            if (! is_null($fqcns[$i]['data']->textView) && View::exists($fqcns[$i]['data']->textView)) {
538
                                $fqcns[$i]['text_view_path'] = View($fqcns[$i]['data']->textView)->getPath();
539
                                $fqcns[$i]['text_view'] = $fqcns[$i]['data']->textView;
540
                            }
541
                        }
542
543
                        // break if you have one class per file (psr-4 compliant)
544
                        // otherwise you'll need to handle class constants (Foo::class)
545
                        break;
546
                    }
547
                }
548
            }
549
550
            $collection = collect($fqcns)->map(function ($mailable) {
551
                return $mailable;
552
            })->reject(function ($object) {
553
                return ! method_exists($object['namespace'], 'build');
554
            });
555
556
            return $collection;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $collection returns the type Illuminate\Support\Collection which is incompatible with the documented return type array.
Loading history...
557
        }
558
    }
559
560
    /**
561
     * Handle Mailable Constructor arguments and pass the fake ones.
562
     *
563
     * @param $mailable
564
     * @return object|void
565
     *
566
     * @throws \ReflectionException
567
     */
568
    public static function handleMailableViewDataArgs($mailable)
569
    {
570
        if (method_exists($mailable, '__construct')) {
571
            $reflection = new ReflectionClass($mailable);
572
573
            $params = $reflection->getConstructor()->getParameters();
574
575
            DB::beginTransaction();
576
577
            $eloquentFactory = app(EloquentFactory::class);
578
579
            $args = collect($params)->map(function ($param) {
580
                if ($param->getType() !== null) {
581
                    if (class_exists($param->getType()->getName())) {
582
                        $parameters = [
583
                            'is_instance' => true,
584
                            'instance' => $param->getType()->getName(),
585
                        ];
586
                    } elseif ($param->getType()->getName() === 'array') {
587
                        $parameters = [
588
                            'is_array' => true,
589
                            'arg' => $param->getName(),
590
                        ];
591
                    } else {
592
                        $parameters = $param->name;
593
                    }
594
595
                    return $parameters;
596
                }
597
598
                return $param->name;
599
            });
600
601
            $resolvedTypeHints = [];
602
603
            foreach ($args->all() as $arg) {
604
                if (is_array($arg)) {
605
                    if (isset($arg['is_instance'])) {
606
                        $model = $arg['instance'];
607
608
                        self::$traversed = 0;
609
610
                        $factoryModel = self::resolveFactory($eloquentFactory, $model);
611
612
                        $resolvedTypeHints[] = $factoryModel
613
                            ? self::hydrateRelations($eloquentFactory, $factoryModel)
614
                            : $factoryModel;
615
                    } elseif (isset($arg['is_array'])) {
616
                        $resolvedTypeHints[] = [];
617
                    } else {
618
                        return;
619
                    }
620
                } else {
621
                    $resolvedTypeHints[] = self::getMissingParams($arg, $params);
622
                }
623
            }
624
625
            $reflector = new ReflectionClass($mailable);
626
627
            if ($args->isNotEmpty()) {
628
                $resolvedTypeHints = array_filter($resolvedTypeHints);
629
630
                return $reflector->newInstanceArgs($resolvedTypeHints);
631
            }
632
633
            DB::rollBack();
634
        }
635
    }
636
637
    /**
638
     * Gets any missing params that may not be collectable in the reflection.
639
     *
640
     * @param  string  $arg  the argument string|array
641
     * @param  array  $params  the reflection param list
642
     * @return array|string|\ReeceM\Mocker\Mocked
643
     */
644
    private static function getMissingParams($arg, $params)
645
    {
646
        /**
647
         * Determine if a builtin type can be found.
648
         * Not a string or object as a Mocked::class can work there.
649
         *
650
         * getName() is undocumented alternative to casting to string.
651
         * https://www.php.net/manual/en/class.reflectiontype.php#124658
652
         *
653
         * @var \ReflectionNamedType|null $reflection
654
         */
655
        $reflection = collect($params)->where('name', $arg)->first()->getType() ?? null;
656
657
        try {
658
            if (is_null($reflection)) {
659
                return new Mocked($arg, \ReeceM\Mocker\Utils\VarStore::singleton());
660
            }
661
662
            $type = version_compare(phpversion(), '7.1', '>=')
663
                ? $reflection->getName()
664
                : /** @scrutinizer ignore-deprecated */ $reflection->__toString();
665
666
            return array_key_exists($type, self::TYPES)
667
                ? self::TYPES[$type]
668
                : new Mocked($arg, \ReeceM\Mocker\Utils\VarStore::singleton());
669
        } catch (\Exception $e) {
670
            return $arg;
671
        }
672
    }
673
674
    /**
675
     * @param $mailable
676
     * @param $mailable_data
677
     * @return array|Collection
678
     *
679
     * @throws \ReflectionException
680
     */
681
    private static function getMailableViewData($mailable, $mailable_data)
682
    {
683
        $traitProperties = [];
684
685
        $data = new ReflectionClass($mailable);
686
687
        foreach ($data->getTraits() as $trait) {
688
            foreach ($trait->getProperties(ReflectionProperty::IS_PUBLIC) as $traitProperty) {
689
                $traitProperties[] = $traitProperty->name;
690
            }
691
        }
692
693
        $properties = $data->getProperties(ReflectionProperty::IS_PUBLIC);
694
        $allProps = [];
695
696
        foreach ($properties as $prop) {
697
            if ($prop->class == $data->getName() || $prop->class == get_parent_class($data->getName()) &&
698
                    get_parent_class($data->getName()) != 'Illuminate\Mail\Mailable' && ! $prop->isStatic()) {
699
                $allProps[] = $prop->name;
700
            }
701
        }
702
703
        $obj = self::buildMailable($mailable);
704
705
        if ($obj === null) {
706
            $obj = [];
707
708
            return collect($obj);
709
        }
710
711
        $classProps = array_diff($allProps, $traitProperties);
712
713
        $withFuncData = collect($obj->viewData)->keys();
714
715
        $mailableData = collect($classProps)->merge($withFuncData);
716
717
        $data = $mailableData->map(function ($parameter) use ($mailable_data) {
718
            return [
719
                'key' => $parameter,
720
                'value' => property_exists($mailable_data, $parameter) ? $mailable_data->$parameter : null,
721
                'data' => property_exists($mailable_data, $parameter) ? self::viewDataInspect($mailable_data->$parameter) : null,
722
            ];
723
        });
724
725
        return $data->all();
726
    }
727
728
    /**
729
     * @param $param
730
     * @return array
731
     */
732
    protected static function viewDataInspect($param): ?array
733
    {
734
        if ($param instanceof \Illuminate\Database\Eloquent\Model) {
735
            return [
736
                'type' => 'model',
737
                'attributes' => collect($param->getAttributes()),
738
            ];
739
        }
740
741
        if ($param instanceof \Illuminate\Database\Eloquent\Collection) {
742
            return [
743
                'type' => 'elequent-collection',
744
                'attributes' => $param->all(),
745
            ];
746
        }
747
748
        if ($param instanceof \Illuminate\Support\Collection) {
749
            return [
750
                'type' => 'collection',
751
                'attributes' => $param->all(),
752
            ];
753
        }
754
755
        return null;
756
    }
757
758
    /**
759
     * @param $mailable
760
     * @return bool
761
     */
762
    protected static function mailable_exists($mailable): bool
763
    {
764
        if (! class_exists($mailable)) {
765
            return false;
766
        }
767
768
        return true;
769
    }
770
771
    /**
772
     * @param $mailable
773
     * @return mixed|void
774
     *
775
     * @throws \ReflectionException
776
     */
777
    protected static function getMarkdownViewName($mailable)
778
    {
779
        if ($mailable === null) {
780
            return;
781
        }
782
783
        $reflection = new ReflectionClass($mailable);
784
785
        $property = $reflection->getProperty('markdown');
786
787
        $property->setAccessible(true);
788
789
        return $property->getValue($mailable);
790
    }
791
792
    /**
793
     * @param $instance
794
     * @param  string  $type
795
     * @return mixed
796
     *
797
     * @throws \Illuminate\Contracts\Container\BindingResolutionException
798
     * @throws \ReflectionException
799
     */
800
    public static function buildMailable($instance, $type = 'call')
801
    {
802
        if ($type === 'call') {
803
            if (self::handleMailableViewDataArgs($instance) !== null) {
804
                return app()->call([self::handleMailableViewDataArgs($instance), 'build']);
805
            }
806
807
            return app()->call([new $instance, 'build']);
808
        }
809
810
        return app()->make($instance);
811
    }
812
813
    /**
814
     * @param $simpleview
815
     * @param $view
816
     * @param  bool  $template
817
     * @param  null  $instance
0 ignored issues
show
Documentation Bug introduced by
Are you sure the doc-type for parameter $instance is correct as it would always require null to be passed?
Loading history...
818
     * @return string|void
819
     *
820
     * @throws \Illuminate\Contracts\Container\BindingResolutionException
821
     * @throws \ReflectionException
822
     */
823
    public static function renderPreview($simpleview, $view, $template = false, $instance = null)
824
    {
825
        if (! View::exists($view)) {
826
            return;
827
        }
828
829
        if (! $template) {
830
            $obj = self::buildMailable($instance);
831
            $viewData = $obj->viewData;
832
            $_data = array_merge($instance->buildViewData(), $viewData);
0 ignored issues
show
Bug introduced by
The method buildViewData() does not exist on null. ( Ignorable by Annotation )

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

832
            $_data = array_merge($instance->/** @scrutinizer ignore-call */ buildViewData(), $viewData);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
833
834
            foreach ($_data as $key => $value) {
835
                if (! is_object($value)) {
836
                    $_data[$key] = '<span class="maileclipse-key" title="Variable">'.$key.'</span>';
837
                }
838
            }
839
        } else {
840
            $_data = [];
841
        }
842
843
        $_view = $view;
844
845
        try {
846
            if ($simpleview) {
847
                return htmlspecialchars_decode(view($_view, $_data)->render());
848
            }
849
850
            $_md = self::buildMailable(Markdown::class, 'make');
851
852
            return htmlspecialchars_decode($_md->render($_view, $_data));
853
        } catch (ErrorException $e) {
854
            $error = '<div class="alert alert-warning">
855
	    	<h5 class="alert-heading">Error:</h5>
856
	    	<p>'.$e->getMessage().'</p>
857
	    	</div>';
858
859
            if ($template) {
860
                $error .= '<div class="alert alert-info">
861
				<h5 class="alert-heading">Notice:</h5>
862
				<p>You can\'t add variables within a template editor because they are undefined until you bind the template with a mailable that actually has data.</p>
863
	    	</div>';
864
            }
865
866
            return $error;
867
        }
868
    }
869
870
    /**
871
     * Class name has to satisfy those rules.
872
     *
873
     * https://www.php.net/manual/en/language.oop5.basic.php#language.oop5.basic.class
874
     * https://www.php.net/manual/en/reserved.keywords.php
875
     *
876
     * @param $input
877
     * @return string|false class name or false on failure
878
     */
879
    public static function generateClassName($input)
880
    {
881
        $suffix = 'Mail';
882
883
        if (strtolower($input) === strtolower($suffix)) {
884
            return false;
885
        }
886
887
        // Avoid MailMail as a class name suffix
888
        $suffix = substr_compare($input, 'mail', -4, 4, true) === 0
889
            ? ''
890
            : $suffix;
891
892
        /**
893
         * - Suffix is needed to avoid usage of reserved word.
894
         * - Str::slug will remove all forbidden characters.
895
         */
896
        $name = Str::studly(Str::slug($input, '_')).$suffix;
897
898
        /**
899
         * Removal of reserved keywords.
900
         */
901
        if (! preg_match('/^[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*$/', $name) ||
902
            substr_compare($name, $suffix, -strlen($suffix), strlen($suffix), true) !== 0
903
        ) {
904
            return false;
905
        }
906
907
        return $name;
908
    }
909
910
    /**
911
     * Resolves the factory result for a model and returns the hydrated instance.
912
     *
913
     * @todo Allow the values for the Make command to be passed down by the pkg.
914
     *
915
     * @param $eloquentFactory
916
     * @param $model
917
     * @return null|object
918
     */
919
    private static function resolveFactory($eloquentFactory, $model): ?object
920
    {
921
        if (! config('maileclipse.factory')) {
922
            return app($model);
923
        }
924
925
        // factory builder backwards compatibility
926
        if (isset($eloquentFactory[$model]) && function_exists('factory')) {
927
            return call_user_func_array('factory', [$model])->make();
928
        }
929
930
        /** @var array|false $modelHasFactory */
931
        $modelHasFactory = class_uses($model);
932
933
        if (isset($modelHasFactory['Illuminate\Database\Eloquent\Factories\HasFactory'])) {
934
            return $model::factory()->make();
935
        }
936
937
        return null;
938
    }
939
940
    /**
941
     * single level relation resolving for a model.
942
     * It will load the relations or set to mocked class.
943
     *
944
     * @param  mixed  $eloquentFactory
945
     * @param  mixed  $factoryModel
946
     * @return null|object
947
     */
948
    private static function hydrateRelations($eloquentFactory, $factoryModel): ?object
949
    {
950
        if (config('maileclipse.relations.relation_depth', 1) === 0) {
951
            return $factoryModel;
952
        }
953
954
        if (self::$traversed >= 5) {
955
            Log::warning('[MailEclipse]: more than 5 calls to relation loader', ['last_model' => get_class($factoryModel) ?? 'model unknown']);
956
            self::$traversed = 6;
957
958
            return $factoryModel;
959
        }
960
961
        $model = new ReflectionClass(config('maileclipse.relations.model', \Illuminate\Foundation\Auth\User::class));
962
963
        self::$traversed += 1;
964
965
        collect((new ReflectionClass($factoryModel))->getMethods())
966
            ->filter(function (\ReflectionMethod $method) use ($model) {
967
                return ! $model->hasMethod($method->getName());
968
            })
969
            ->filter(function (\ReflectionMethod $method) use ($factoryModel) {
970
                if ($method->getNumberOfParameters() >= 1) {
971
                    return false;
972
                }
973
974
                $parents = rescue(function () use ($method, $factoryModel) {
975
                    $methodName = $method->getName();
976
977
                    return $method->hasReturnType()
978
                        ? class_parents($method->getReturnType()->getName())
0 ignored issues
show
Bug introduced by
The method getName() does not exist on ReflectionType. It seems like you code against a sub-type of ReflectionType such as ReflectionNamedType. ( Ignorable by Annotation )

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

978
                        ? class_parents($method->getReturnType()->/** @scrutinizer ignore-call */ getName())
Loading history...
979
                        : class_parents($factoryModel->$methodName());
980
                }, [], false);
981
982
                return isset($parents["Illuminate\Database\Eloquent\Relations\Relation"]);
983
            })
984
            ->each(function (\ReflectionMethod $relationName) use (&$factoryModel, $eloquentFactory) {
985
                $factoryModel = self::loadRelations($relationName->getName(), $factoryModel, $eloquentFactory);
986
            });
987
988
        return $factoryModel;
989
    }
990
991
    /**
992
     * Load the relations for the model and the relation.
993
     *
994
     * @todo Account for the many type relations, link back to parent model for belongsTo
995
     *
996
     * @param  mixed  $relationName
997
     * @param  mixed  $factoryModel
998
     * @param  mixed|null  $eloquentFactory
999
     * @return null|object
1000
     */
1001
    public static function loadRelations($relationName, $factoryModel, $eloquentFactory = null): ?object
1002
    {
1003
        try {
1004
            $factoryModel->load($relationName);
1005
1006
            $loadIfIterable = is_iterable($factoryModel->$relationName) && count($factoryModel->$relationName) <= 0;
1007
1008
            if (is_null($factoryModel->$relationName) || $loadIfIterable) {
1009
                $related = $factoryModel->$relationName()->getRelated();
1010
                $relatedFactory = self::resolveFactory($eloquentFactory, get_class($related));
1011
1012
                if (self::$traversed <= config('maileclipse.relations.relation_depth')) {
1013
                    if (! $loadIfIterable) {
1014
                        $relatedFactory = self::hydrateRelations($eloquentFactory, $relatedFactory);
1015
                    } else {
1016
                        $models = collect();
1017
1018
                        for ($loads = 0; $loads < 3; $loads++) {
1019
                            $models->push(self::hydrateRelations($eloquentFactory, $relatedFactory));
1020
                        }
1021
1022
                        $relatedFactory = $models;
1023
                    }
1024
                }
1025
1026
                $factoryModel->setRelation(
1027
                    $relationName,
1028
                    $relatedFactory
1029
                );
1030
            }
1031
        } catch (\Throwable $th) {
1032
            $factoryModel->setRelation(
1033
                $relationName,
1034
                new Mocked(
1035
                    'relation_is_null',
1036
                    \ReeceM\Mocker\Utils\VarStore::singleton()
1037
                )
1038
            );
1039
        } finally {
1040
            return $factoryModel;
1041
        }
1042
    }
1043
1044
    /**
1045
     * @param  string  $name
1046
     * @return bool|\Illuminate\Contracts\View\Factory|\Illuminate\Contracts\View\View
1047
     *
1048
     * @throws \ReflectionException
1049
     */
1050
    public static function renderMailable(string $name)
1051
    {
1052
        $mailable = self::getMailable('name', $name)->first();
1053
1054
        if (collect($mailable['data'])->isEmpty()) {
1055
            return false;
1056
        }
1057
1058
        $mailableInstance = self::resolveMailableInstance($mailable);
1059
1060
        $view = $mailable['markdown'] ?? $mailable['data']->view;
1061
1062
        if (view()->exists($view)) {
1063
            return ($mailableInstance)->render();
1064
        }
1065
1066
        return view(self::VIEW_NAMESPACE.'::previewerror', ['errorMessage' => 'No template associated with this mailable.']);
1067
    }
1068
1069
    /**
1070
     * @param  string  $name
1071
     * @param  string  $recipient
1072
     */
1073
    public static function sendTest(string $name, string $recipient): void
1074
    {
1075
        $mailable = self::getMailable('name', $name)->first();
1076
1077
        $mailableInstance = self::resolveMailableInstance($mailable);
1078
1079
        $mailableInstance = self::setMailableSendTestRecipient($mailableInstance, $recipient);
1080
1081
        Mail::send($mailableInstance);
1082
    }
1083
1084
    /**
1085
     * @param $mailable
1086
     * @param  string  $email
1087
     * @return mixed
1088
     */
1089
    public static function setMailableSendTestRecipient($mailable, string $email)
1090
    {
1091
        $mailable->to($email);
1092
        $mailable->cc([]);
1093
        $mailable->bcc([]);
1094
1095
        return $mailable;
1096
    }
1097
1098
    /**
1099
     * @param $mailable
1100
     * @return object|void
1101
     *
1102
     * @throws \ReflectionException
1103
     */
1104
    private static function resolveMailableInstance($mailable)
1105
    {
1106
        if (self::handleMailableViewDataArgs($mailable['namespace']) !== null) {
1107
            $mailableInstance = self::handleMailableViewDataArgs($mailable['namespace']);
1108
        } else {
1109
            $mailableInstance = new $mailable['namespace'];
1110
        }
1111
1112
        return $mailableInstance;
1113
    }
1114
}
1115