深入研究 larvael 源码第二天
12


前言

上一篇文章,研究了larvael框架入口文件 index.php,这一篇深入研究 获取框架实例底层代码
至于 composer自动加载源码分析请到这观看


bootstrap/app.php文件分析

 $app = new Illuminate\Foundation\Application(
    realpath(__DIR__.'/../')
);

$app->singleton(
    Illuminate\Contracts\Http\Kernel::class,
    App\Http\Kernel::class
);

$app->singleton(
    Illuminate\Contracts\Console\Kernel::class,
    App\Console\Kernel::class
);

$app->singleton(
    Illuminate\Contracts\Debug\ExceptionHandler::class,
    App\Exceptions\Handler::class
);

return $app;
  1. 第一步 获取应用实例
  2. 第二步 绑定框架核心 http内核,控制台内核,异常处理单例


Illuminate\Foundation\Application文件分析

1. 引入

use Closure;   //PHP 匿名函数类
use RuntimeException; //php 运行异常类
use Illuminate\Support\Arr; //框架支持的arr类
use Illuminate\Support\Str;//框架支持的str类
use Illuminate\Http\Request;//框架http 请求类
use Illuminate\Support\Collection;//框架支持的集合类
use Illuminate\Container\Container;//框架容器核心类
use Illuminate\Filesystem\Filesystem;//框架文件系统的文件类
use Illuminate\Log\LogServiceProvider;//框架日志组件日志服务提供者
use Illuminate\Support\ServiceProvider;//框架支持的服务提供者
use Illuminate\Events\EventServiceProvider;//框架事件系统的事件服务提供者
use Illuminate\Routing\RoutingServiceProvider;//框架路由组件的路由服务提供者
use Symfony\Component\HttpKernel\HttpKernelInterface;//Symfony框架http内核接口
use Symfony\Component\HttpKernel\Exception\HttpException;//Symfony框架http内核异常下http异常类
use Illuminate\Contracts\Http\Kernel as HttpKernelContract;//框架http内核契约
use Illuminate\Foundation\Bootstrap\LoadEnvironmentVariables;//框架基础加载环境变量类
use Symfony\Component\HttpFoundation\Request as SymfonyRequest;//Symfony框架http请求类
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;//Symfony框架http内核异常类
use Illuminate\Contracts\Foundation\Application as ApplicationContract;//框架契约基础类

2. 定义类定义属性

class Application extends Container implements ApplicationContract, HttpKernelInterface
{//定义Application对象继承Container,实现ApplicationContract, HttpKernelInterface
    const VERSION = '5.7.12'; //版本号
    protected $basePath;//基本路径
    protected $hasBeenBootstrapped = false;//应用程序是否已被自举。
    protected $booted = false;//引用程序是否已启动
    protected $bootingCallbacks = [];//引导回调的数组
    protected $bootedCallbacks = [];//启动回调的数组
    protected $terminatingCallbacks = [];//终止回调的数组
    protected $serviceProviders = [];//注册的服务提供者
    protected $loadedProviders = [];//加载的服务提供者的名称
    protected $deferredServices = [];//延迟的服务提供者的名称
    protected $databasePath;//数据库路径
    protected $storagePath;//存储路径
    protected $environmentPath;//环境变量路径
    protected $environmentFile = '.env';//环境变量文件
    protected $namespace;//应用程序名称空间

3. 构造函数,初始化处理

1、setBasePath // 设置应用基础路径
2、registerBaseBindings // 绑定容器中的所有应用程序路径
3、registerBaseServiceProviders // 注册基本绑定
4、registerCoreContainerAliases //注册容器核心类别名

public function __construct($basePath = null)
    {
        if ($basePath) {
            $this->setBasePath($basePath);
        }
        $this->registerBaseBindings();
        $this->registerBaseServiceProviders();
        $this->registerCoreContainerAliases();
    }

4. 设置基础路径

1、bindPathsInContainer

    public function setBasePath($basePath)
    {
        $this->basePath = rtrim($basePath, '\/');
        $this->bindPathsInContainer();
        return $this;
    }

5. 绑定容器中的所有应用程序路径

protected function bindPathsInContainer()
    {
        $this->instance('path', $this->path());
        $this->instance('path.base', $this->basePath());
        $this->instance('path.lang', $this->langPath());
        $this->instance('path.config', $this->configPath());
        $this->instance('path.public', $this->publicPath());
        $this->instance('path.storage', $this->storagePath());
        $this->instance('path.database', $this->databasePath());
        $this->instance('path.resources', $this->resourcePath());
        $this->instance('path.bootstrap', $this->bootstrapPath());
}

6. 将基本绑定注册到容器中

1、static::setInstance
2、instance

       protected function registerBaseBindings()
     {
        static::setInstance($this);//设置全局容器可用实例
        $this->instance('app', $this);//绑定当前实例到app
        $this->instance(Container::class, $this);//绑定当前实例到Container
        $this->instance(PackageManifest::class, new PackageManifest(
            new Filesystem, $this->basePath(), $this->getCachedPackagesPath()
        ));//文件实例绑定到PackageManifest
    }

7. 注册所有的基础服务提供者

1、register

protected function registerBaseServiceProviders()
    {
        $this->register(new EventServiceProvider($this));//1. 事件服务提供者        
        $this->register(new LogServiceProvider($this));// 2. 日志服务提供者
        $this->register(new RoutingServiceProvider($this));  //3. 路由服务提供者
    }

8. 注册方法剖析

1、getProvider
2、resolveProvider
3、bind
4、singleton
5、markAsRegistered
6、bootProvider
7、参考链接

   public function register($provider, $force = false)
    {
        //判断该服务提供者是否已注册,已经注册直接返回实例
        if (($registered = $this->getProvider($provider)) && ! $force) {
            return $registered;
        }
        //如果传入一个字符串(类名)
        if (is_string($provider)) {
            //直接去实例化(直白点就是new 这个类)
            $provider = $this->resolveProvider($provider);
        }
        //如果该服务提供者存在register方法,直接执行
        if (method_exists($provider, 'register')) {
            $provider->register();
        }
       //如果服务提供者包含bindings属性,则进行
        if (property_exists($provider, 'bindings')) {
            foreach ($provider->bindings as $key => $value) {
                $this->bind($key, $value);
            }
        }
        //如果服务提供者包含singletons属性,则进行
        if (property_exists($provider, 'singletons')) {
            foreach ($provider->singletons as $key => $value) {
                $this->singleton($key, $value);
            }
        }
        //标记该程序为已注册,加入已注册服务提供者数组和已加载服务提供者数组
        $this->markAsRegistered($provider);
        //如果程序已启动,调用服务提供者boot方法
        if ($this->booted) {
            $this->bootProvider($provider);
        }
        //返回实例
        return $provider;
    }

9. 获取版本号

        public function version()

10. 注册回调以在加载环境之后运行

    public function afterLoadingEnvironment(Closure $callback)
    {
        return $this->afterBootstrapping(
            LoadEnvironmentVariables::class, $callback
        );
    }

11. 注册回调以在引导程序之后运行

   public function afterBootstrapping($bootstrapper, Closure $callback)
    {
        $this['events']->listen('bootstrapped: '.$bootstrapper, $callback);//监听引导后事件
    }

12. 注册回调在引导程序之前运行

  public function beforeBootstrapping($bootstrapper, Closure $callback)
    {
        $this['events']->listen('bootstrapping: '.$bootstrapper, $callback);//监听引导前事件
    }

13. 是否程序自举

  public function hasBeenBootstrapped()
    {
        return $this->hasBeenBootstrapped;
    }

14. 获取app目录路径

 public function path($path = '')
    {
        return $this->basePath.DIRECTORY_SEPARATOR.'app'.($path ? DIRECTORY_SEPARATOR.$path : $path);
    }

15. 获取laravel根路径

public function basePath($path = '')
    {
        return $this->basePath.($path ? DIRECTORY_SEPARATOR.$path : $path);
    }

16. 获取bootstrap路径

public function bootstrapPath($path = '')
    {
        return $this->basePath.DIRECTORY_SEPARATOR.'bootstrap'.($path ? DIRECTORY_SEPARATOR.$path : $path);
    }

17. 获取config路径

public function configPath($path = '')
    {
        return $this->basePath.DIRECTORY_SEPARATOR.'config'.($path ? DIRECTORY_SEPARATOR.$path : $path);
    }

18. 获取database路径

 public function databasePath($path = '')
    {
        return ($this->databasePath ?: $this->basePath.DIRECTORY_SEPARATOR.'database').($path ? DIRECTORY_SEPARATOR.$path : $path);
    }

19. 设置databsee路径

public function useDatabasePath($path)
    {
        $this->databasePath = $path;

        $this->instance('path.database', $path);//添加到容器里

        return $this;
    }

20. 获取资源resource路径

public function resourcePath($path = '')
    {
        return $this->basePath.DIRECTORY_SEPARATOR.'resources'.($path ? DIRECTORY_SEPARATOR.$path : $path);
    }

21. 获取public路径

public function publicPath()
    {
        return $this->basePath.DIRECTORY_SEPARATOR.'public';
    }

22. 获取语言包路径

public function langPath()
    {
        return $this->resourcePath().DIRECTORY_SEPARATOR.'lang';
    }

23. 获取strorge的路径

public function storagePath()
    {
        return $this->storagePath ?: $this->basePath.DIRECTORY_SEPARATOR.'storage';
    }

24. 设置strorge的路径

 public function useStoragePath($path)
    {
        $this->storagePath = $path;
        $this->instance('path.storage', $path);
        return $this;
    }

26. 获取env路径

public function environmentPath()
    {
        return $this->environmentPath ?: $this->basePath;
    }

27. 设置env路径

    public function useEnvironmentPath($path)
    {
        $this->environmentPath = $path;

        return $this;
    }

28. 设置env文件

public function loadEnvironmentFrom($file)
    {
        $this->environmentFile = $file;

        return $this;
    }

29. 获取env文件

 public function environmentFile()
    {
        return $this->environmentFile ?: '.env';
    }

29. 获取env文件路径

public function environmentFilePath()
    {
        return $this->environmentPath().DIRECTORY_SEPARATOR.$this->environmentFile();
    }

30. 获取应用env配置

public function environment()
    {
        //判断是否有参数
        if (func_num_args() > 0) {
            $patterns = is_array(func_get_arg(0)) ? func_get_arg(0) : func_get_args();
            return Str::is($patterns, $this['env']);//字符串搜索
        }
        return $this['env'];
    }

31. 环境是否本地开发

 public function isLocal()
    {
        return $this['env'] === 'local';
    }

32. 检测应用当前环境 detect方法详解

 public function detectEnvironment(Closure $callback)
    {
        $args = $_SERVER['argv'] ?? null;//获取cli模式下的参数

        return $this['env'] = (new EnvironmentDetector)->detect($callback, $args);//
    }

33. 确定应用是否在控制台运行

 public function runningInConsole()
    {
        if (isset($_ENV['APP_RUNNING_IN_CONSOLE'])) {
            return $_ENV['APP_RUNNING_IN_CONSOLE'] === 'true';
        }

        return php_sapi_name() === 'cli' || php_sapi_name() === 'phpdbg';//cli模式或者phpdbg调试模式
    }

34. 确定应用是否运行在unit单元测试

public function runningUnitTests()
    {
        return $this['env'] === 'testing';
    }

35. 注册所有配置的提供程序

public function registerConfiguredProviders()
    {
        $providers = Collection::make($this->config['app.providers'])//创建一个集合实例从app.php获取到的服务提供者数组
                        ->partition(function ($provider) {
                            return Str::startsWith($provider, 'Illuminate\\');//判断是否已$needles字符串开头
                        });//*使用给定的回调或键将集合划分为两个数组。一部分框架核心一部分自定义

       //将所有包的所有服务提供者类名填充到1的位置
        $providers->splice(1, 0, [$this->make(PackageManifest::class)->providers()]);获取所。

        //存入服务提供者缓存文件 
        (new ProviderRepository($this, new Filesystem, $this->getCachedServicesPath()))
                    ->load($providers->collapse()->toArray());
    }

36. 如果已注册服务提供程序实例存在,则获取该实例


    public function getProvider($provider)
    {
        return array_values($this->getProviders($provider))[0] ?? null;
    }

37. 获取所有注册的服务提供者

 public function getProviders($provider)
    {
        $name = is_string($provider) ? $provider : get_class($provider);//获取类名称

        return Arr::where($this->serviceProviders, function ($value) use ($name) {
            return $value instanceof $name;//判断是否是实例
        });
    }

38. 类名解析服务提供者实例

 public function resolveProvider($provider)
    {
        return new $provider($this);
    }

39. 标记程序为已注册

protected function markAsRegistered($provider)
    {
        $this->serviceProviders[] = $provider;

        $this->loadedProviders[get_class($provider)] = true;
    }

40. 加载所有延迟服务提供者

 public function loadDeferredProviders()
    {

        foreach ($this->deferredServices as $service => $provider) {
            $this->loadDeferredProvider($service);//循环加载延迟服务提供者
        }
        $this->deferredServices = [];//将延迟服务提供者置为空
    }

41. 加载延迟服务提供者

public function loadDeferredProvider($service)
    {
       //判断是否属于延迟服务提供者
        if (! isset($this->deferredServices[$service])) {
            return;
        }
        //判断是否已加载
        if (! isset($this->loadedProviders[$provider])) {
            //注册延迟服务提供者
            $this->registerDeferredProvider($provider, $service);
        }
    }

42. 注册延迟服务提供者

public function registerDeferredProvider($provider, $service = null)
    {
        if ($service) {
            unset($this->deferredServices[$service]);//将该服务清空出延时服务提供者数组
        }
        $this->register($instance = new $provider($this));//注册服务
        if (! $this->booted) {
            $this->booting(function () use ($instance) {
                $this->bootProvider($instance);//启动服务调用boot
            });
        }
    }

43. 从容器中解析实例

1、getAlias
2、parent::make

 public function make($abstract, array $parameters = [])
    {
        $abstract = $this->getAlias($abstract);//获取别名
         //如果是延迟服务提供者并且没有实例化就加载延迟程序
        if (isset($this->deferredServices[$abstract]) && ! isset($this->instances[$abstract])) {
            $this->loadDeferredProvider($abstract);
        }

        return parent::make($abstract, $parameters);//使用父类方法解析实例并返回
    }

44. 确定给定的抽象类型是否已经被绑定

1、parent::bound

 public function bound($abstract)
    {
        return isset($this->deferredServices[$abstract]) || parent::bound($abstract);
    }

45. 应用是否已启动

public function isBooted()
    {
        return $this->booted;
    }

46. 启动应用

1、fireAppCallbacks

    public function boot()
    {
        if ($this->booted) {
            return;
        }
        //调用引导回调
        $this->fireAppCallbacks($this->bootingCallbacks);
        array_walk($this->serviceProviders, function ($p) {
            $this->bootProvider($p);//启动服务提供者
        });
        $this->booted = true;//设置程序已启动
        //调用启动后
        $this->fireAppCallbacks($this->bootedCallbacks);
    }

47. 启动服务提供者

protected function bootProvider(ServiceProvider $provider)
    {
        if (method_exists($provider, 'boot')) {
            return $this->call([$provider, 'boot']);
        }
    }

48. 设置应用引导回调数组

public function booting($callback)
    {
        $this->bootingCallbacks[] = $callback;
    }

49. 设置应用启动回调数组

public function booted($callback)
    {
        $this->bootedCallbacks[] = $callback;

        if ($this->isBooted()) {
            $this->fireAppCallbacks([$callback]);//如果已启动,调用回调
        }
    }

50. 调用程序回调

protected function fireAppCallbacks(array $callbacks)
    {
        foreach ($callbacks as $callback) {
            call_user_func($callback, $this);
        }
    }

51. handle

public function handle(SymfonyRequest $request, $type = self::MASTER_REQUEST, $catch = true)
    {
        return $this[HttpKernelContract::class]->handle(Request::createFromBase($request));
    }

52. shouldSkipMiddleware

1、bound
2、make

public function shouldSkipMiddleware()
    {
        return $this->bound('middleware.disable') &&
               $this->make('middleware.disable') === true;
    }```
### 53. <a name="getCachedServicesPath">获取缓存服务路径</a>
1、<a href="#bootstrapPath">bootstrapPath</a>
```php
public function getCachedServicesPath()
    {
        return $this->bootstrapPath().'/cache/services.php';
    }

54. 获取packages.php的路径

1、bootstrapPath

public function getCachedPackagesPath()
    {
        return $this->bootstrapPath().'/cache/packages.php';
    }

55. 确定是否缓存了应用程序配置

1、getCachedConfigPath

public function configurationIsCached()
    {
        return file_exists($this->getCachedConfigPath());
    }

56. 获取config文件路径

1、bootstrapPath

 public function getCachedConfigPath()
    {
        return $this->bootstrapPath().'/cache/config.php';
    }

57. 确定是否缓存了路由配置

1、getCachedRoutesPath

public function routesAreCached()
    {
        return $this['files']->exists($this->getCachedRoutesPath());
    }

58. 获取路由缓存路径

1、bootstrapPath

 public function getCachedRoutesPath()
    {
        return $this->bootstrapPath().'/cache/routes.php';
    }

59. 确定应用程序当前是否停机进行维护

1、storagePath

public function isDownForMaintenance()
    {
        return file_exists($this->storagePath().'/framework/down');
    }

60. 抛出http异常

 public function abort($code, $message = '', array $headers = [])
    {
        if ($code == 404) {
            throw new NotFoundHttpException($message);
        }

        throw new HttpException($code, $message, null, $headers);
    }

61. 设置程序终止的回调

public function terminating(Closure $callback)
    {
        $this->terminatingCallbacks[] = $callback;

        return $this;
    }

62. 执行终止程序回调

 public function terminate()
    {
        foreach ($this->terminatingCallbacks as $terminating) {
            $this->call($terminating);
        }
    }

63. 获取所有已加载的服务提供者

 public function getLoadedProviders()
    {
        return $this->loadedProviders;
    }

64. 获取所有延迟加载服务提供者

 public function getDeferredServices()
    {
        return $this->deferredServices;
    }

65. 设置延迟服务提供者

public function setDeferredServices(array $services)
    {
        $this->deferredServices = $services;
    }

66. 添加延迟服务提供者

public function addDeferredServices(array $services)
    {
        $this->deferredServices = array_merge($this->deferredServices, $services);
    }

67. 验证是否是延迟服务提供者

 public function isDeferredService($service)
    {
        return isset($this->deferredServices[$service]);
    }

68. 设置服务门面命名空间

1、AliasLoader::setFacadeNamespace

public function provideFacades($namespace)
    {
        AliasLoader::setFacadeNamespace($namespace);
    }

69. 获取当前区域设置

public function getLocale()
    {
        return $this['config']->get('app.locale');
    }

70. 设置当前区域

public function setLocale($locale)
    {
        $this['config']->set('app.locale', $locale);

        $this['translator']->setLocale($locale);

        $this['events']->dispatch(new Events\LocaleUpdated($locale));
    }

71. 判断当前区域

 public function isLocale($locale)
    {
        return $this->getLocale() == $locale;
    }

72. 在容器中注册核心类别名

1、alias

    public function registerCoreContainerAliases()
    {
        foreach ([
            'app'                  => [\Illuminate\Foundation\Application::class, \Illuminate\Contracts\Container\Container::class, \Illuminate\Contracts\Foundation\Application::class,  \Psr\Container\ContainerInterface::class],
            'auth'                 => [\Illuminate\Auth\AuthManager::class, \Illuminate\Contracts\Auth\Factory::class],
            'auth.driver'          => [\Illuminate\Contracts\Auth\Guard::class],
            'blade.compiler'       => [\Illuminate\View\Compilers\BladeCompiler::class],
            'cache'                => [\Illuminate\Cache\CacheManager::class, \Illuminate\Contracts\Cache\Factory::class],
            'cache.store'          => [\Illuminate\Cache\Repository::class, \Illuminate\Contracts\Cache\Repository::class],
            'config'               => [\Illuminate\Config\Repository::class, \Illuminate\Contracts\Config\Repository::class],
            'cookie'               => [\Illuminate\Cookie\CookieJar::class, \Illuminate\Contracts\Cookie\Factory::class, \Illuminate\Contracts\Cookie\QueueingFactory::class],
            'encrypter'            => [\Illuminate\Encryption\Encrypter::class, \Illuminate\Contracts\Encryption\Encrypter::class],
            'db'                   => [\Illuminate\Database\DatabaseManager::class],
            'db.connection'        => [\Illuminate\Database\Connection::class, \Illuminate\Database\ConnectionInterface::class],
            'events'               => [\Illuminate\Events\Dispatcher::class, \Illuminate\Contracts\Events\Dispatcher::class],
            'files'                => [\Illuminate\Filesystem\Filesystem::class],
            'filesystem'           => [\Illuminate\Filesystem\FilesystemManager::class, \Illuminate\Contracts\Filesystem\Factory::class],
            'filesystem.disk'      => [\Illuminate\Contracts\Filesystem\Filesystem::class],
            'filesystem.cloud'     => [\Illuminate\Contracts\Filesystem\Cloud::class],
            'hash'                 => [\Illuminate\Hashing\HashManager::class],
            'hash.driver'          => [\Illuminate\Contracts\Hashing\Hasher::class],
            'translator'           => [\Illuminate\Translation\Translator::class, \Illuminate\Contracts\Translation\Translator::class],
            'log'                  => [\Illuminate\Log\LogManager::class, \Psr\Log\LoggerInterface::class],
            'mailer'               => [\Illuminate\Mail\Mailer::class, \Illuminate\Contracts\Mail\Mailer::class, \Illuminate\Contracts\Mail\MailQueue::class],
            'auth.password'        => [\Illuminate\Auth\Passwords\PasswordBrokerManager::class, \Illuminate\Contracts\Auth\PasswordBrokerFactory::class],
            'auth.password.broker' => [\Illuminate\Auth\Passwords\PasswordBroker::class, \Illuminate\Contracts\Auth\PasswordBroker::class],
            'queue'                => [\Illuminate\Queue\QueueManager::class, \Illuminate\Contracts\Queue\Factory::class, \Illuminate\Contracts\Queue\Monitor::class],
            'queue.connection'     => [\Illuminate\Contracts\Queue\Queue::class],
            'queue.failer'         => [\Illuminate\Queue\Failed\FailedJobProviderInterface::class],
            'redirect'             => [\Illuminate\Routing\Redirector::class],
            'redis'                => [\Illuminate\Redis\RedisManager::class, \Illuminate\Contracts\Redis\Factory::class],
            'request'              => [\Illuminate\Http\Request::class, \Symfony\Component\HttpFoundation\Request::class],
            'router'               => [\Illuminate\Routing\Router::class, \Illuminate\Contracts\Routing\Registrar::class, \Illuminate\Contracts\Routing\BindingRegistrar::class],
            'session'              => [\Illuminate\Session\SessionManager::class],
            'session.store'        => [\Illuminate\Session\Store::class, \Illuminate\Contracts\Session\Session::class],
            'url'                  => [\Illuminate\Routing\UrlGenerator::class, \Illuminate\Contracts\Routing\UrlGenerator::class],
            'validator'            => [\Illuminate\Validation\Factory::class, \Illuminate\Contracts\Validation\Factory::class],
            'view'                 => [\Illuminate\View\Factory::class, \Illuminate\Contracts\View\Factory::class],
        ] as $key => $aliases) {
            foreach ($aliases as $alias) {
                $this->alias($key, $alias);
            }
        }
    }

73. 清除所有绑定和解决实例的容器

1、parent::flush

    public function flush()
    {
        parent::flush();
        $this->buildStack = [];
        $this->loadedProviders = [];
        $this->bootedCallbacks = [];
        $this->bootingCallbacks = [];
        $this->deferredServices = [];
        $this->reboundCallbacks = [];
        $this->serviceProviders = [];
        $this->resolvingCallbacks = [];
        $this->afterResolvingCallbacks = [];
        $this->globalResolvingCallbacks = [];
    }

74. 获取命名空间

    public function getNamespace()
    {
        if (! is_null($this->namespace)) {
            return $this->namespace;
        }
        $composer = json_decode(file_get_contents(base_path('composer.json')), true);
        foreach ((array) data_get($composer, 'autoload.psr-4') as $namespace => $path) {
            foreach ((array) $path as $pathChoice) {
                if (realpath(app_path()) == realpath(base_path().'/'.$pathChoice)) {
                    return $this->namespace = $namespace;
                }
            }
        }
        throw new RuntimeException('Unable to detect application namespace.');
    }

75. 运行给定的引导类数组

public function bootstrapWith(array $bootstrappers)
    {
        $this->hasBeenBootstrapped = true;

        foreach ($bootstrappers as $bootstrapper) {
            $this['events']->dispatch('bootstrapping: '.$bootstrapper, [$this]);

            $this->make($bootstrapper)->bootstrap($this);

            $this['events']->dispatch('bootstrapped: '.$bootstrapper, [$this]);
        }
    }
《L01 基础入门》
我们将带你从零开发一个项目并部署到线上,本课程教授 Web 开发中专业、实用的技能,如 Git 工作流、Laravel Mix 前端工作流等。
《L05 电商实战》
从零开发一个电商项目,功能包括电商后台、商品 & SKU 管理、购物车、订单管理、支付宝支付、微信支付、订单退款流程、优惠券等
讨论数量: 1
Zerin

???这就完了

2个月前

请勿发布不友善或者负能量的内容。与人为善,比聪明更重要!