Passed
Pull Request — master (#157)
by
unknown
04:55
created

MailEclipse::hydrateRelations()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 36
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Importance

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

338
            return self::renderPreview($simpleview, self::VIEW_NAMESPACE.'::draft.'.$viewName, $template, /** @scrutinizer ignore-type */ $instance);
Loading history...
339
        }
340
341
        return false;
342
    }
343
344
    /**
345
     * @param $instance
346
     * @param $view
347
     * @return string|void
348
     */
349
    public static function previewMarkdownHtml($instance, $view)
350
    {
351
        return self::renderPreview($instance, $view);
352
    }
353
354
    /**
355
     * @param $mailableName
356
     * @return array|bool
357
     */
358
    public static function getMailableTemplateData($mailableName)
359
    {
360
        $mailable = self::getMailable('name', $mailableName);
361
362
        if ($mailable->isEmpty()) {
363
            return false;
364
        }
365
366
        $templateData = collect($mailable->first())->only(['markdown', 'view_path', 'text_view_path', 'text_view', 'view_data', 'data', 'namespace'])->all();
367
368
        $templateExists = $templateData['view_path'] !== null;
369
        $textTemplateExists = $templateData['text_view_path'] !== null;
370
371
        if ($templateExists) {
372
            $viewPathParams = collect($templateData)->union([
373
374
                'text_template' => $textTemplateExists ? file_get_contents($templateData['text_view_path']) : null,
375
                'template' => file_get_contents($templateData['view_path']),
376
                'markdowned_template' => self::markdownedTemplate($templateData['view_path']),
377
                'template_name' => $templateData['markdown'] ?? $templateData['data']->view,
378
                'is_markdown' => $templateData['markdown'] !== null,
379
            ])->all();
380
381
            return $viewPathParams;
382
        }
383
384
        return $templateData;
385
    }
386
387
    /**
388
     * @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...
389
     * @return JsonResponse
390
     */
391
    public static function generateMailable($request = null): JsonResponse
392
    {
393
        $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

393
        $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...
394
395
        if ($name === false) {
396
            return response()->json([
397
                'status' => 'error',
398
                'message' => 'Wrong name format.',
399
            ]);
400
        }
401
402
        if (! $request->has('force') && ! self::getMailable('name', $name)->isEmpty()) {
403
            return response()->json([
404
                'status' => 'error',
405
                'message' => 'This mailable name already exists. names should be unique! to override it, enable "force" option.',
406
            ]);
407
        }
408
409
        if (strtolower($name) === 'mailable') {
410
            return response()->json([
411
                'status' => 'error',
412
                'message' => 'You cannot use "mailable" as a mailable name',
413
            ]);
414
        }
415
416
        $params = collect([
417
            'name' => $name,
418
        ]);
419
420
        if ($request->input('markdown')) {
421
            $params->put('--markdown', $request->markdown);
422
        }
423
424
        if ($request->has('force')) {
425
            $params->put('--force', true);
426
        }
427
428
        $exitCode = Artisan::call('make:mail', $params->all());
429
430
        if ($exitCode > -1) {
431
            return response()->json([
432
                'status' => 'ok',
433
                'message' => 'Mailable Created<br> <small>Reloading...<small>',
434
            ]);
435
        }
436
437
        return response()->json([
438
439
            'status' => 'error',
440
            'message' => 'mailable not created successfully',
441
442
        ]);
443
    }
444
445
    /**
446
     * Get Mailables list.
447
     *
448
     * @return array
449
     * @throws \ReflectionException
450
     */
451
    protected static function mailablesList()
452
    {
453
        $fqcns = [];
454
455
        if (! file_exists(config('maileclipse.mailables_dir'))) {
456
            return;
457
        } else {
458
            $allFiles = new RecursiveIteratorIterator(new RecursiveDirectoryIterator(config('maileclipse.mailables_dir')));
459
            $phpFiles = new RegexIterator($allFiles, '/\.php$/');
460
            $i = 0;
461
462
            foreach ($phpFiles as $phpFile) {
463
                $i++;
464
                $content = file_get_contents($phpFile->getRealPath());
465
                $tokens = token_get_all($content);
466
                $namespace = '';
467
                for ($index = 0; isset($tokens[$index]); $index++) {
468
                    if (! isset($tokens[$index][0])) {
469
                        continue;
470
                    }
471
                    if (T_NAMESPACE === $tokens[$index][0]) {
472
                        $index += 2; // Skip namespace keyword and whitespace
473
                        while (isset($tokens[$index]) && is_array($tokens[$index])) {
474
                            $namespace .= $tokens[$index++][1];
475
                        }
476
                    }
477
                    if (T_CLASS === $tokens[$index][0] && T_WHITESPACE === $tokens[$index + 1][0] && T_STRING === $tokens[$index + 2][0]) {
478
                        $index += 2; // Skip class keyword and whitespace
479
480
                        [$name, $extension] = explode('.', $phpFile->getFilename());
481
482
                        $mailableClass = $namespace.'\\'.$tokens[$index][1];
483
484
                        if (! self::mailable_exists($mailableClass)) {
485
                            continue;
486
                        }
487
488
                        $reflector = new ReflectionClass($mailableClass);
489
490
                        if ($reflector->isAbstract()) {
491
                            continue;
492
                        }
493
494
                        $mailable_data = self::buildMailable($mailableClass);
495
496
                        if (! is_null(self::handleMailableViewDataArgs($mailableClass))) {
497
                            $mailable_view_data = self::getMailableViewData(self::handleMailableViewDataArgs($mailableClass), $mailable_data);
498
                        } else {
499
                            $mailable_view_data = self::getMailableViewData(new $mailableClass, $mailable_data);
500
                        }
501
502
                        $fqcns[$i]['data'] = $mailable_data;
503
                        $fqcns[$i]['markdown'] = self::getMarkdownViewName($mailable_data);
504
                        $fqcns[$i]['name'] = $name;
505
                        $fqcns[$i]['namespace'] = $mailableClass;
506
                        $fqcns[$i]['filename'] = $phpFile->getFilename();
507
                        $fqcns[$i]['modified'] = $phpFile->getCTime();
508
                        $fqcns[$i]['viewed'] = $phpFile->getATime();
509
                        $fqcns[$i]['view_data'] = $mailable_view_data;
510
                        // $fqcns[$i]['view_data'] = [];
511
                        $fqcns[$i]['path_name'] = $phpFile->getPathname();
512
                        $fqcns[$i]['readable'] = $phpFile->isReadable();
513
                        $fqcns[$i]['writable'] = $phpFile->isWritable();
514
                        $fqcns[$i]['view_path'] = null;
515
                        $fqcns[$i]['text_view_path'] = null;
516
517
                        if (! is_null($fqcns[$i]['markdown']) && View::exists($fqcns[$i]['markdown'])) {
518
                            $fqcns[$i]['view_path'] = View($fqcns[$i]['markdown'])->getPath();
519
                        }
520
521
                        if (! is_null($fqcns[$i]['data'])) {
522
                            if (! is_null($fqcns[$i]['data']->view) && View::exists($fqcns[$i]['data']->view)) {
523
                                $fqcns[$i]['view_path'] = View($fqcns[$i]['data']->view)->getPath();
524
                            }
525
526
                            if (! is_null($fqcns[$i]['data']->textView) && View::exists($fqcns[$i]['data']->textView)) {
527
                                $fqcns[$i]['text_view_path'] = View($fqcns[$i]['data']->textView)->getPath();
528
                                $fqcns[$i]['text_view'] = $fqcns[$i]['data']->textView;
529
                            }
530
                        }
531
532
                        // break if you have one class per file (psr-4 compliant)
533
                        // otherwise you'll need to handle class constants (Foo::class)
534
                        break;
535
                    }
536
                }
537
            }
538
539
            $collection = collect($fqcns)->map(function ($mailable) {
540
                return $mailable;
541
            })->reject(function ($object) {
542
                return ! method_exists($object['namespace'], 'build');
543
            });
544
545
            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...
546
        }
547
    }
548
549
    /**
550
     * Handle Mailable Constructor arguments and pass the fake ones.
551
     * @param $mailable
552
     * @return object|void
553
     * @throws \ReflectionException
554
     */
555
    public static function handleMailableViewDataArgs($mailable)
556
    {
557
        if (method_exists($mailable, '__construct')) {
558
            $reflection = new ReflectionClass($mailable);
559
560
            $params = $reflection->getConstructor()->getParameters();
561
562
            DB::beginTransaction();
563
564
            $eloquentFactory = app(EloquentFactory::class);
565
566
            $args = collect($params)->map(function ($param) {
567
                if ($param->getType() !== null) {
568
                    if (class_exists($param->getType()->getName())) {
569
                        $parameters = [
570
                            'is_instance' => true,
571
                            'instance' => $param->getType()->getName(),
572
                        ];
573
                    } elseif ($param->getType()->getName() === 'array') {
574
                        $parameters = [
575
                            'is_array' => true,
576
                            'arg' => $param->getName(),
577
                        ];
578
                    } else {
579
                        $parameters = $param->name;
580
                    }
581
582
                    return $parameters;
583
                }
584
585
                return $param->name;
586
            });
587
588
            $resolvedTypeHints = [];
589
590
            foreach ($args->all() as $arg) {
591
                if (is_array($arg)) {
592
                    if (isset($arg['is_instance'])) {
593
                        $model = $arg['instance'];
594
595
                        self::$traversed = 0;
596
597
                        $factoryModel = self::resolveFactory($eloquentFactory, $model);
598
599
                        $resolvedTypeHints[] = $factoryModel
600
                            ? self::hydrateRelations($eloquentFactory, $factoryModel)
601
                            : $factoryModel;
602
                    } elseif (isset($arg['is_array'])) {
603
                        $resolvedTypeHints[] = [];
604
                    } else {
605
                        return;
606
                    }
607
                } else {
608
                    $resolvedTypeHints[] = self::getMissingParams($arg, $params);
609
                }
610
            }
611
612
            $reflector = new ReflectionClass($mailable);
613
614
            if ($args->isNotEmpty()) {
615
                $resolvedTypeHints = array_filter($resolvedTypeHints);
616
617
                return $reflector->newInstanceArgs($resolvedTypeHints);
618
            }
619
620
            DB::rollBack();
621
        }
622
    }
623
624
    /**
625
     * Gets any missing params that may not be collectable in the reflection.
626
     *
627
     * @param string $arg the argument string|array
628
     * @param array $params the reflection param list
629
     *
630
     * @return array|string|\ReeceM\Mocker\Mocked
631
     */
632
    private static function getMissingParams($arg, $params)
633
    {
634
        /**
635
         * Determine if a builtin type can be found.
636
         * Not a string or object as a Mocked::class can work there.
637
         *
638
         * getName() is undocumented alternative to casting to string.
639
         * https://www.php.net/manual/en/class.reflectiontype.php#124658
640
         *
641
         * @var \ReflectionType $reflection
642
         */
643
        $reflection = collect($params)->where('name', $arg)->first()->getType();
644
645
        if (version_compare(phpversion(), '7.1', '>=')) {
646
            $type = ! is_null($reflection)
647
                ? self::TYPES[$reflection->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

647
                ? self::TYPES[$reflection->/** @scrutinizer ignore-call */ getName()]
Loading history...
648
                : null;
649
        } else {
650
            $type = ! is_null($reflection)
651
                ? self::TYPES[/** @scrutinizer ignore-deprecated */ $reflection->__toString()]
652
                : null;
653
        }
654
655
        try {
656
            return ! is_null($type)
657
                    ? $type
658
                    : new Mocked($arg, \ReeceM\Mocker\Utils\VarStore::singleton());
659
        } catch (\Exception $e) {
660
            return $arg;
661
        }
662
    }
663
664
    /**
665
     * @param $mailable
666
     * @param $mailable_data
667
     * @return array|Collection
668
     * @throws \ReflectionException
669
     */
670
    private static function getMailableViewData($mailable, $mailable_data)
671
    {
672
        $traitProperties = [];
673
674
        $data = new ReflectionClass($mailable);
675
676
        foreach ($data->getTraits() as $trait) {
677
            foreach ($trait->getProperties(ReflectionProperty::IS_PUBLIC) as $traitProperty) {
678
                $traitProperties[] = $traitProperty->name;
679
            }
680
        }
681
682
        $properties = $data->getProperties(ReflectionProperty::IS_PUBLIC);
683
        $allProps = [];
684
685
        foreach ($properties as $prop) {
686
            if ($prop->class == $data->getName() || $prop->class == get_parent_class($data->getName()) &&
687
                    get_parent_class($data->getName()) != 'Illuminate\Mail\Mailable' && ! $prop->isStatic()) {
688
                $allProps[] = $prop->name;
689
            }
690
        }
691
692
        $obj = self::buildMailable($mailable);
693
694
        if ($obj === null) {
695
            $obj = [];
696
697
            return collect($obj);
698
        }
699
700
        $classProps = array_diff($allProps, $traitProperties);
701
702
        $withFuncData = collect($obj->viewData)->keys();
703
704
        $mailableData = collect($classProps)->merge($withFuncData);
705
706
        $data = $mailableData->map(function ($parameter) use ($mailable_data) {
707
            return [
708
                'key' => $parameter,
709
                'value' => property_exists($mailable_data, $parameter) ? $mailable_data->$parameter : null,
710
                'data' => property_exists($mailable_data, $parameter) ? self::viewDataInspect($mailable_data->$parameter) : null,
711
            ];
712
        });
713
714
        return $data->all();
715
    }
716
717
    /**
718
     * @param $param
719
     * @return array
720
     */
721
    protected static function viewDataInspect($param): ?array
722
    {
723
        if ($param instanceof \Illuminate\Database\Eloquent\Model) {
724
            return [
725
                'type' => 'model',
726
                'attributes' => collect($param->getAttributes()),
727
            ];
728
        }
729
730
        if ($param instanceof \Illuminate\Database\Eloquent\Collection) {
731
            return [
732
                'type' => 'elequent-collection',
733
                'attributes' => $param->all(),
734
            ];
735
        }
736
737
        if ($param instanceof \Illuminate\Support\Collection) {
738
            return [
739
                'type' => 'collection',
740
                'attributes' => $param->all(),
741
            ];
742
        }
743
744
        return null;
745
    }
746
747
    /**
748
     * @param $mailable
749
     * @return bool
750
     */
751
    protected static function mailable_exists($mailable): bool
752
    {
753
        if (! class_exists($mailable)) {
754
            return false;
755
        }
756
757
        return true;
758
    }
759
760
    /**
761
     * @param $mailable
762
     * @return mixed|void
763
     * @throws \ReflectionException
764
     */
765
    protected static function getMarkdownViewName($mailable)
766
    {
767
        if ($mailable === null) {
768
            return;
769
        }
770
771
        $reflection = new ReflectionClass($mailable);
772
773
        $property = $reflection->getProperty('markdown');
774
775
        $property->setAccessible(true);
776
777
        return $property->getValue($mailable);
778
    }
779
780
    /**
781
     * @param $instance
782
     * @param string $type
783
     * @return mixed
784
     * @throws \Illuminate\Contracts\Container\BindingResolutionException
785
     * @throws \ReflectionException
786
     */
787
    public static function buildMailable($instance, $type = 'call')
788
    {
789
        if ($type === 'call') {
790
            if (self::handleMailableViewDataArgs($instance) !== null) {
791
                return app()->call([self::handleMailableViewDataArgs($instance), 'build']);
792
            }
793
794
            return app()->call([new $instance, 'build']);
795
        }
796
797
        return app()->make($instance);
798
    }
799
800
    /**
801
     * @param $simpleview
802
     * @param $view
803
     * @param bool $template
804
     * @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...
805
     * @return string|void
806
     * @throws \Illuminate\Contracts\Container\BindingResolutionException
807
     * @throws \ReflectionException
808
     */
809
    public static function renderPreview($simpleview, $view, $template = false, $instance = null)
810
    {
811
        if (! View::exists($view)) {
812
            return;
813
        }
814
815
        if (! $template) {
816
            $obj = self::buildMailable($instance);
817
            $viewData = $obj->viewData;
818
            $_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

818
            $_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...
819
820
            foreach ($_data as $key => $value) {
821
                if (! is_object($value)) {
822
                    $_data[$key] = '<span class="maileclipse-key" title="Variable">'.$key.'</span>';
823
                }
824
            }
825
        } else {
826
            $_data = [];
827
        }
828
829
        $_view = $view;
830
831
        try {
832
            if ($simpleview) {
833
                return htmlspecialchars_decode(view($_view, $_data)->render());
0 ignored issues
show
Bug introduced by
It seems like view($_view, $_data)->render() can also be of type array; however, parameter $string of htmlspecialchars_decode() does only seem to accept string, 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

833
                return htmlspecialchars_decode(/** @scrutinizer ignore-type */ view($_view, $_data)->render());
Loading history...
834
            }
835
836
            $_md = self::buildMailable(Markdown::class, 'make');
837
838
            return htmlspecialchars_decode($_md->render($_view, $_data));
839
        } catch (ErrorException $e) {
840
            $error = '<div class="alert alert-warning">
841
	    	<h5 class="alert-heading">Error:</h5>
842
	    	<p>'.$e->getMessage().'</p>
843
	    	</div>';
844
845
            if ($template) {
846
                $error .= '<div class="alert alert-info">
847
				<h5 class="alert-heading">Notice:</h5>
848
				<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>
849
	    	</div>';
850
            }
851
852
            return $error;
853
        }
854
    }
855
856
    /**
857
     * Class name has to satisfy those rules.
858
     *
859
     * https://www.php.net/manual/en/language.oop5.basic.php#language.oop5.basic.class
860
     * https://www.php.net/manual/en/reserved.keywords.php
861
     *
862
     * @param $input
863
     * @return string|false class name or false on failure
864
     */
865
    public static function generateClassName($input)
866
    {
867
        $suffix = 'Mail';
868
869
        if (strtolower($input) === strtolower($suffix)) {
870
            return false;
871
        }
872
873
        // Avoid MailMail as a class name suffix
874
        $suffix = substr_compare($input, 'mail', -4, 4, true) === 0
875
            ? ''
876
            : $suffix;
877
878
        /**
879
         * - Suffix is needed to avoid usage of reserved word.
880
         * - Str::slug will remove all forbidden characters.
881
         */
882
        $name = Str::studly(Str::slug($input, '_')).$suffix;
883
884
        /**
885
         * Removal of reserved keywords.
886
         */
887
        if (! preg_match('/^[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*$/', $name) ||
888
            substr_compare($name, $suffix, -strlen($suffix), strlen($suffix), true) !== 0
889
        ) {
890
            return false;
891
        }
892
893
        return $name;
894
    }
895
896
    /**
897
     * Resolves the factory result for a model and returns the hydrated instance.
898
     *
899
     * @todo Allow the values for the Make command to be passed down by the pkg.
900
     *
901
     * @param $eloquentFactory
902
     * @param $model
903
     *
904
     * @return null|object
905
     */
906
    private static function resolveFactory($eloquentFactory, $model): ?object
907
    {
908
        if (! config('maileclipse.factory')) {
909
            return app($model);
910
        }
911
912
        // factory builder backwards compatibility
913
        if (isset($eloquentFactory[$model]) && function_exists('factory')) {
914
            return call_user_func_array('factory', [$model])->make();
915
        }
916
917
        /** @var array|false $modelHasFactory */
918
        $modelHasFactory = class_uses($model);
919
920
        if (isset($modelHasFactory['Illuminate\Database\Eloquent\Factories\HasFactory'])) {
921
            return $model::factory()->make();
922
        }
923
924
        return null;
925
    }
926
927
    /**
928
     * single level relation resolving for a model.
929
     * It will load the relations or set to mocked class.
930
     *
931
     * @param mixed $eloquentFactory
932
     * @param mixed $factoryModel
933
     *
934
     * @return null|object
935
     */
936
    private static function hydrateRelations($eloquentFactory, $factoryModel): ?object
937
    {
938
        if (config('maileclipse.relation_depth') === 0) {
939
            return $factoryModel;
940
        }
941
942
        if (self::$traversed >= 5) {
943
            Log::warning('[MailEclipse]: more than 5 calls to relation loader', ['last_model' => get_class($factoryModel) ?? null]);
944
            self::$traversed = 6;
945
946
            return $factoryModel;
947
        }
948
949
        $model = new ReflectionClass(Model::class);
950
951
        self::$traversed += 1;
952
953
        collect((new ReflectionClass($factoryModel))->getMethods())
954
            ->pluck('name')
955
            ->diff(collect($model->getMethods())->pluck('name'))
956
            ->filter(function ($method) use ($factoryModel) {
957
                return rescue(
958
                    function () use ($factoryModel, $method) {
959
                        $parents = class_parents($factoryModel->$method());
960
961
                        return isset($parents["Illuminate\Database\Eloquent\Relations\Relation"]);
962
                    },
963
                    false,
964
                    false
965
                );
966
            })
967
            ->each(function ($relationName) use (&$factoryModel, $eloquentFactory) {
968
                $factoryModel = self::loadRelations($relationName, $factoryModel, $eloquentFactory);
969
            });
970
971
        return $factoryModel;
972
    }
973
974
    /**
975
     * Load the relations for the model and the relation.
976
     *
977
     * @todo Account for the many type relations, link back to parent model for belongsTo
978
     *
979
     * @param mixed $relationName
980
     * @param mixed $factoryModel
981
     * @param mixed|null $eloquentFactory
982
     *
983
     * @return null|object
984
     */
985
    public static function loadRelations($relationName, $factoryModel, $eloquentFactory = null): ?object
986
    {
987
        try {
988
            $factoryModel->load($relationName);
989
990
            $loadIfIterable = is_iterable($factoryModel->$relationName) && count($factoryModel->$relationName) <= 0;
991
992
            if (is_null($factoryModel->$relationName) || $loadIfIterable) {
993
                $related = $factoryModel->$relationName()->getRelated();
994
                $relatedFactory = self::resolveFactory($eloquentFactory, get_class($related));
995
996
                if (self::$traversed <= config('maileclipse.relation_depth')) {
997
                    if (! $loadIfIterable) {
998
                        $relatedFactory = self::hydrateRelations($eloquentFactory, $relatedFactory);
999
                    } else {
1000
                        $models = collect();
1001
1002
                        for ($loads = 0; $loads < 3; $loads++) {
1003
                            $models->push(self::hydrateRelations($eloquentFactory, $relatedFactory));
1004
                        }
1005
1006
                        $relatedFactory = $models;
1007
                    }
1008
                }
1009
1010
                $factoryModel->setRelation(
1011
                    $relationName,
1012
                    $relatedFactory
1013
                );
1014
            }
1015
        } catch (\Throwable $th) {
1016
            $factoryModel->setRelation(
1017
                $relationName,
1018
                new Mocked(
1019
                    'relation_is_null',
1020
                    \ReeceM\Mocker\Utils\VarStore::singleton()
1021
                )
1022
            );
1023
        } finally {
1024
            return $factoryModel;
1025
        }
1026
    }
1027
1028
    /**
1029
     * @param string $name
1030
     * @return bool|\Illuminate\Contracts\View\Factory|\Illuminate\Contracts\View\View
1031
     * @throws \ReflectionException
1032
     */
1033
    public static function renderMailable(string $name)
1034
    {
1035
        $mailable = self::getMailable('name', $name)->first();
1036
1037
        if (collect($mailable['data'])->isEmpty()) {
1038
            return false;
1039
        }
1040
1041
        $mailableInstance = self::resolveMailableInstance($mailable);
1042
1043
        $view = $mailable['markdown'] ?? $mailable['data']->view;
1044
1045
        if (view()->exists($view)) {
1046
            return ($mailableInstance)->render();
1047
        }
1048
1049
        return view(self::VIEW_NAMESPACE.'::previewerror', ['errorMessage' => 'No template associated with this mailable.']);
1050
    }
1051
1052
    /**
1053
     * @param string $name
1054
     * @param string $recipient
1055
     */
1056
    public static function sendTest(string $name, string $recipient): void
1057
    {
1058
        $mailable = self::getMailable('name', $name)->first();
1059
1060
        $mailableInstance = self::resolveMailableInstance($mailable);
1061
1062
        $mailableInstance = self::setMailableSendTestRecipient($mailableInstance, $recipient);
1063
1064
        Mail::send($mailableInstance);
1065
    }
1066
1067
    /**
1068
     * @param $mailable
1069
     * @param string $email
1070
     * @return mixed
1071
     */
1072
    public static function setMailableSendTestRecipient($mailable, string $email)
1073
    {
1074
        $mailable->to($email);
1075
        $mailable->cc([]);
1076
        $mailable->bcc([]);
1077
1078
        return $mailable;
1079
    }
1080
1081
    /**
1082
     * @param $mailable
1083
     * @return object|void
1084
     * @throws \ReflectionException
1085
     */
1086
    private static function resolveMailableInstance($mailable)
1087
    {
1088
        if (self::handleMailableViewDataArgs($mailable['namespace']) !== null) {
1089
            $mailableInstance = self::handleMailableViewDataArgs($mailable['namespace']);
1090
        } else {
1091
            $mailableInstance = new $mailable['namespace'];
1092
        }
1093
1094
        return $mailableInstance;
1095
    }
1096
}
1097