tahona/spark-mvc 问题修复 & 功能扩展

解决BUG、新增功能、兼容多环境部署,快速响应你的开发需求

邮箱:yvsm@zunyunkeji.com | QQ:316430983 | 微信:yvsm316

tahona/spark-mvc

Composer 安装命令:

composer require tahona/spark-mvc

包简介

Web MVC Framework

关键字:

README 文档

README

What is this repository for?

Very fast PHP framework with autoloading and annotations. All configuration is handled by annotations and beans.

How it works

Framework automatically load beans from app/src path it just need to be annotated with one annotation of: @Component, @Service, @Configuration or @Repository.

Bean injection can be achieved with @Inject annotation.

Initialization is done on the first request. The second request execute Controllers action, that has already injected beans in it.

Request execution time is very small because it's time of user code execution only.

Thanks to this, for real production code we got 20ms-40ms per request for dynamic data.

Quick Start

For quick start donwload example project: https://github.com/primosz67/spark-mvc-example

Index php - explained

app/public/index.php

error_reporting(E_ALL);

define("__ROOT__", __DIR__ . "/../../");
define("__VENDOR__", "../../vendor");

require __VENDOR__ . "/autoload.php";

Note: If you will use doctrine db framework add here line - "AnnotationRegistry::registerLoader('class_exists');"

Framework setup:

$profileName ="someNameProfile";
$engine = new Engine("przemek_config", $profileName, __ROOT__ . "app");
$engine->run();

Configuration

app/src/MyAppConfig.php

/**
 * @Configuration()
 * @EnableApcuBeanCache(resetParam="reset")
 */
class MyAppConfig {
}
  • resetParam - parameter for clearing cache. (GET http://app.com?reset). It's for development environment and should be deleted for production.

Controller

1. Standard Controller with annotation controller

app/src/MyAppController.php

/**
* @Controller
*/
class MyAppController {

    /**
     * @Path("/index", method="GET")
     */
    public function indexAction(Request $request) {
        return new PlainViewModel("Hello World");
    }

   /**
     * @Path("/save", method="POST")
     */
    public function saveAction(SomeForm $form, Request $req, $beanName) {
        return new JsonViewModel(array(
            "user"=>"TODO"
        ));
    }
}

Go to localhost/get or localhost/index;

  • Request is injected to method by Type
  • SomeForm is filled by "POST" form params using "spark-form" module.
  • $beanName - framework can inject any bean or service needed for action by name or type.
2. Rest Controller
/**
* @RestController
*/
class MyRestController {

    /**
     * @Path("/index")
     */
    public function indexAction(Request $request) {
        return new SomeResultDto();
    }
}

Note: All methods in RestController will resolve to JSON.

SomeResultDto - will resolve to json.

3. Old method of creating Controller

app/src/MyAppController.php

class MyAppController extends Controller {


    /**
     * @Path("/index")
     */
    public function indexAction() {
        return new PlainViewModel("Hello World");
    }

    /**
     * @Path("/get")
     */
    public function getAction() {
        return new JsonViewModel(array(
            "user"=>"TODO"
        ));
    }

    /**
     * @Path("/newView")
     */
     public function showNewViewAction() {
        return new ViewModel(array(
            "user"=>"TODO"
        ));
     }

}

Go to localhost/get or localhost/index;

2. Define bean for autoload.

Injection

1. Define bean for autoload.
/**
* @Service()
*/
class UserService {
    //...some methods
}

Note: for enabled apcu @EnableApcuBeanCache("reset") to autoload injection, call http://website?reset

2. Define other beans and inject out bean.
/**
* @Component()
*/
class AddUserHandler {

    /**
    * @Inject()
    * var UserService
    */
    private $userService;

     /**
     * @PostConstruct()
     */
     public function init() {
        $this->userService->doSomething();
     }
}
3.Inject in controller
class UserController extends Controller {

    /**
    * @Inject()
    * var UserService
    */
    private $userService;

    /**
     * @Path("/newView")
     */
     public function showNewViewAction() {
        return new ViewModel(array(
            "users"=>$this->userService->getAllUsers()
        ));
     }
}
4.Inject in action method of controller
/**
* @Controller
*/
class UserController {

    /**
     * @Path("/newView")
     */
     public function showNewViewAction(Request $request, UserService $userService ) {
        return new ViewModel(array(
            "users"=>$userService->getAllUsers()
        ));
     }
}

View

apc/view/{controller package}/{controllerName (without "Controller")}/{action}.tpl

  1. For app/src/MyAppController@showNewViewAction we get: apc/view/myapp/showNewView.tpl
  2. For app/src/some/serious/package/controller/MyAppController@showNewViewAction we get: apc/view/some/serious/package/myapp/showNewView.tpl

Keywords action and controller are deleted by default.

Smarty built-in path plugins

Function path can be used in tpl file (example: index.tpl)

{path path="/user/register"}

Command will resolve to : www.example.com/user/register

{path path="RegisterController@registerAction"}

Command will resolve to : www.example.com/user/register

  • com.example.RegisterController - optional Controller class Name
  • registerAction - method name in RegisterController
{path path="@removeAction@id:4,type:blogpost"}

Command will resolve to : www.example.com/blogpost/remove/4

  • {controller} - current request controller will be used
  • removeAction - method name in RegisterController
  • id:4,type:blogpost - params will resolve /{type}/remove/{id} to it's final form

Apcu Bean Cache

if @EnableApcuBeanCache annotation is added with @Configuration the only way to reset beans and init them once more is by requestin localhost:80?reset (GET parameter "reset").

Mailer

  • @EnableMailer -TODO
  • spark.mailer.enabled (true/false)- property

@Annotations

The heart of Spark Framework.

  • @Component,@Service,@Repository,@Configuration - do the same thing, but purpose is the key.
  • @Bean
  • @PostConstruct
  • @Inject
  • @Bean

Application Parameters

$this->config

Base parameters: app.path - pierwsza ścieżka do katalogu /app src.path - ścieżka do katalogu /app/src app.paths - ścieżka albo ścieżki do katalogu /app w różnych kontekstach

to fetch parameters:

 $appPath = $this-config->getProperty("app.path");

update or set Params

 $this-config->setProperty("customModule.some.property.", "/my/new/path");

Custom module loading

If you create common module to use in other project remember to create beans by @Bean annotation. It will be easier to add new module in one go.

In some your app configuration add others OtherModuleConfig.

/**
* @Bean
*/
public function otherBeanConfiguration () {
    return new OtherModuleConfig();
}

All @Bean annotation in OtherModuleConfig will be created and inject to your classes.

Multiple DataBase connection (example)

Handle multiple connections. To create Doctrine's EntityManager you can use simple EntityManagerFactory.

    /**
     * @Inject()
     * @var EntityManagerFactory
     */
    private $entityManagerFactory;


    /**
    * @Bean()
    */
    public function entityManager() {
        return $this->entityManagerFactory->createEntityManager($this->getDataSource());
    }

    /**
    * @Bean()
    */
    public function superEntityManager() {
        return $this->entityManagerFactory->createEntityManager($this->getDataSourceSuper());
    }

    public function getDataSource() {
        $dbConfig = new DataSource();
        $dbConfig->setDbname("my-db");
        $dbConfig->setHost("127.0.0.1");
        $dbConfig->setUsername("root");
        $dbConfig->setPassword("test");
        $dbConfig->setPackages([
            "com/myapp/user/domain" //path to doctrine entity
        ]);
        return $dbConfig;
    }

    public function getDataSourceSuper() {
        $dbConfig = new DataSource();
        $dbConfig->setDbname("super");
        $dbConfig->setHost("127.0.0.1");
        $dbConfig->setUsername("root");
        $dbConfig->setPassword("test");
        return $dbConfig;
    }

Note: for using CrudDao with other enityManager than basic use @OverrideInject annotation

/**
* @OverrideInject(oldName="entityManager", newName="houseManager")
*/
class MySuperDao extends CrudDao {
}

Internalization

Bean definition

    /**
    * @Bean
    */
    public function anyPrefixNameMessageResource() {
        return new LangResourcePath(array(
            "pl"=>array(
                "/house/house_pl.properties"
            ),
            "cz"=> array(...),
            "en"=>array(...),
        ));
    }

Where "pl","cz","en" are cookie value with key "lang";

  • Property file /house/house_pl.properties
core.thank.you.message=Thank You {0} and {1}
  • Use in php
    /**
     * @Inject
     * @var langMessageResource
     */
    private $langMessageResource;


    ...

    $this->langMessageResource->get("core.thank.you.message", array("John", "Trevor"));
  • Use in smarty
{lang code="core.thank.you.message" value=["John", "Trevor"]}

Results: Thank You John and Trevor

@Path

Annotation Defonition

@Path(path="/login/{test}", method="get")

Fetch path paramether in Controller class;

$this->getParam("test");

MULTI fetch by component

Example is for dynamic menu module that will popup when new project or classes are added.

    /**
    * @Inject
    */
    private $beanProvider;

    public function getMenuModules() {
        return $beanProvider->getByType(MenuModule::class);
    }

Interceptors

/**
* @Component
*/
class UserInterceptor implements HandlerInterceptor {

    /**
    * @Inject
    */
    private $someUserHolder;

    public function preHandle(Request $request) {
        //Do something like redirect or add values to request or auto-conversion(id to entity);
    }

    public function postHandle(Request $request, ViewModel $viewModel) {
        $viewModel->add("loggedUser", $someUserHolder->getUserFromSession())
    }
}

Command behaviour for PHP cli

First, create class Command implementation

/**
 * @Component()
 */
class ExampleCommand implements Command {

    /**
     * @Inject()
     * @var SomeBean
     */
    private $someBean;

    public function getName() :string{
        return "example:exampleCommandCommand";
    }

    public function execute(InputInterface $in, OutputInterface $out) : void {
        $out->writeln("executing " . $this->getName());

        //Example ....
        $this->someBean->doSomething()
        $out->writeObject($this->someBean->getSomething());


        $out->writeln("finish!");
    }
}

in console execute:

php app/public/index.php command=example:exampleCommand profile=production

Output:

executing example:exampleCommand

object(...)

finish!

Built-in cache service

Great thing for caching DB request or loading files data. Annotation can be used with different cache. Even custom cache bean that implement spark/cache/Cache

How To

In Bean class add @Cache annotation.

/**
 * @Cache(name="cache", key="user {0}.id", time=10)
 */
public function getLeaderByCompany($company){
    return $this->someDao->getByCompanyId($company->getId())
}
  • "name" is a name of bean that implement spark\cache\Cache interface.
  • ApcCache needed for application is added as default name="cache"
  • "time" - optional parameter that is in minutes(10 minutes)
  • "key" parameter is for distinguish cached values

Profiles

$profileName = "production";
$engine = new Engine("przemek_config",$profileName,  __ROOT__ . "app");
@Configuration
@Profile(name="production")
class SomeProductionConfig(){
..
}

@Configuration
@Profile(name="development")
class SomeDevelopmentConfig(){
..
}

In this case SomeDevelopmentConfig won't be added to container and bean declared in it (@Bean) as well.

Error handling - example

class NotFoundErrorHandler extends ExceptionResolver {

    public function doResolveException($ex) {
        if ($ex instanceof RouteNotFoundException || $ex instanceof EntityNotFoundException) {
            ResponseHelper::setCode(HttpCode::$NOT_FOUND);

            $viewModel = new ViewModel();
            $viewModel->setViewName("/house/web/error/notFound");
            return $viewModel;
        }
        return null;
    }

    public function getOrder() {
        return 400;
    }
}

where error handler with order equal 0 , will be first to invoke. If you return Viewmodel the handling will stop and the view will be return as response.

Event Bus

/**
* @Inject
*/
private $eventBus;
...
$this->eventBus->post(new SomeEvent());

Event definition :

class SomeEvent implements Event {...}

Subscription

@Component 
class SomeListener {

  /**
  * @Subscribe
  */
  public function handleSomeCase(SomeEvent $event) {
     //...logic
  }

}

AnnotationHandler

Add Your own annotation

class NewAnnotationHandler extends AnnotationHandler{
    ...
}

Installation - Composer - Speed up

composer dump-autoload -a

Performance - Some numbers

Tested Case: Real project with small database. AB(Apache benchmark) requests 10000 and 1000 parallel connections.

  • Smarty compile: Smarty template rendering for each request (Development mode).
  • Smarty: smarty with production setup.
  • Smarty + @Cache (Redis): request to database are cached with @Cache annotation (we use Redis for caching). It can give big improvement to performance when there is more calls to DB.
Mode Request time (per request)
Smarty compile ~130ms
Smarty ~45ms
Smarty + @Cache (Redis) ~25ms

Installation - Composer

{
    "autoload": {
        "psr-4": {"": "app/src/"}
	},

	"require": {
        "smarty/smarty": "3.1.27",
        "tahona/spark-mvc": "*"
    }
}

tahona/spark-mvc 适用场景与选型建议

tahona/spark-mvc 是一款 基于 PHP 开发的 Composer 扩展包,目前已累计 370 次下载、GitHub Stars 达 5, 最近一次更新时间为 2017 年 01 月 29 日, 在 PHP 生态内属于活跃度较高的组件。

它主要适用于以下技术方向: 「mvc」 等业务场景。在实际项目中,围绕这些方向常见需要落地的问题包括:接口对接、性能调优、并发安全、与既有框架(Laravel / ThinkPHP / Yii / Webman 等)的兼容适配,以及生产环境的日志埋点与稳定性保障。

我们在过去多个企业项目中使用过 tahona/spark-mvc 或与其功能相近的方案,如果你在选型或落地过程中遇到问题,例如 版本兼容、二次改造、私有化封装、与内部系统对接、生产 BUG 排查,欢迎联系我们协助评估。

围绕 tahona/spark-mvc 我们能提供哪些服务?
定制开发 / 二次开发

基于 tahona/spark-mvc 在你已有业务上做功能扩展、字段裁剪、UI 适配、与内部账号 / 权限 / 日志系统的深度对接。

BUG 修复 & 性能优化

线上偶发问题、内存泄漏、慢查询、并发异常等排查修复;针对高流量场景做缓存、队列、索引层面的调优。

项目外包 & 长期维护

承接完整的项目从需求 → 设计 → 开发 → 上线 → 长期运维;也可按月提供技术保姆服务。

yvsm@zunyunkeji.com QQ:316430983 微信:yvsm316 西安尊云信息科技 · 专注 PHP / Go / 分布式系统研发

统计信息

  • 总下载量: 370
  • 月度下载量: 0
  • 日度下载量: 0
  • 收藏数: 5
  • 点击次数: 10
  • 依赖项目数: 1
  • 推荐数: 0

GitHub 信息

  • Stars: 5
  • Watchers: 1
  • Forks: 2
  • 开发语言: PHP

其他信息

  • 授权协议: MIT
  • 更新时间: 2017-01-29