Tag: api

  • Slim 4初步研究

    Slim是一个我很喜欢用的轻框架,我用它为我的任氏有无轩站点提供API服务。这几天趁着放假,想重新“折腾”一下我的站点,于是就开了一个虚拟机,装好了必要的软件,准备开发。

    然后我发现,Slim这个框架已经升级到了4,有了重大的变化。

    参照Slim官方说明创建项目后,目录结构如下(请忽略其中的nbproject目录):

    • app目录:它包含了对于整个应用来说最基本的一些文件。具体说明如下。

    • dependencies.php:它主要是创建应用全局的容器。应用安装时,会生成logger这个实例。数据库链接的实例也是在这里生成的:

    return function (ContainerBuilder $containerBuilder) {
        $containerBuilder->addDefinitions([
            ...
            PDO::class => function (ContainerInterface $c) {
                $settings=$c->get('settings')['db'];
                $host=$settings['host'];
                $user=$settings['user'];
                $pass=$settings['pass'];
                $db=$settings['db'];
    
                $dsn="mysql:host=$host;dbname=$db";
                $conn=new PDO($dsn, $user, $pass);
                $conn->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
                return $conn;
            },
        ]);
    };
    

    注意,我在这里设置了数据库PDO链接的一些属性。以后在应用的任何地方,只要用到PDO这个类的声明,Slim都会在容器中去寻找PDO的实例,从而保证所有的数据库链接都是统一的。

    • middleware.php:中间件我目前在API中还不会用到。按照Slim的说明,中间件可以在应用运行,修改相应的RequestResponse对象。
    • repositories.php:这里注册所有可以成为“仓库”的类。这是Slim 4新增的文件,用来统一管理MVC中的M;但Slim 4抛弃了M,而改用更轻量的仓库。这也是一个容器。如:
    declare(strict_types=1);
    
    use App\Domain\Book\BookRepository;
    use App\Infrastructure\Persistence\Book\DBBookRepository;
    
    use DI\ContainerBuilder;
    
    return function (ContainerBuilder $containerBuilder) {
        $containerBuilder->addDefinitions([
            BookRepository::class => \DI\autowire(DBBookRepository::class),
        ]);
    };

    注:这里用到的一些类会在后续得到说明。不过可以提醒一下,Slim 4用一个接口(BookRepository)定义所有该仓库支持的操作,而用一个实例类(DBBookRepository)对这个接口进行实现。你可以把它们的这种关系类比成C++中的“hpp/cpp”的关系。

    • routes.php:这是常规的路由配置。请注意,Slim4放弃了一个controller中多个action的做法,而是一个action对应一个controller,如下例所示:
    declare(strict_types=1);
    
    use Psr\Http\Message\ResponseInterface as Response;
    use Psr\Http\Message\ServerRequestInterface as Request;
    use Slim\App;
    use Slim\Interfaces\RouteCollectorProxyInterface as Group;
    
    use App\Application\Actions\Index\IndexAction;
    use App\Application\Actions\Book;
    
    return function (App $app) {
        $app->get('/', IndexAction::class);
        $app->group('/summary', function(Group $group) {
            $group->get('', Book\SummaryAction::class);
        });
    };

    于是,/summary这个路由就会调用Book\SummaryAction::class中制定的action函数。

    • settings.php:全局配置文件。数据库的配置也在此处出现:
    return function (ContainerBuilder $containerBuilder) {
        // Global Settings Object
        $containerBuilder->addDefinitions([
            'settings' => [
                'displayErrorDetails' => true, // Should be set to false in production
                'logger' => [
                    'name' => 'slim-app',
                    'path' => isset($_ENV['docker']) ? 'php://stdout' : __DIR__ . '/../logs/app.log',
                    'level' => Logger::DEBUG,
                ],
                //Database connection
                'db' => [
                    'host'  => 'localhost',
                    'user'  => '****',
                    'pass'  => '****',
                    'db'    => '****',
                ],
            ],
        ]);
    };

    总结:app目录中的,都是对应用全局产生影响的文件。

    • public目录:这里是应用的入口文件。一般情况下,对于一个API应用来说,这里没有什么需要修改的。
    • src/Application目录:这里有四个目录。我们重点要关注的是src/Application/Actions目录。

    根据我的理解,针对API要成立的各类实体,开发者可以进行逻辑抽象分类,形成各个action。在具体实现的时候,一般是针对每个实体实现一个抽象的类,然后对每个针对这个实体的操作定义一个action

    比如我的数据库中有“书籍(book)”这个实体,于是我就暂时定义了两个文件:BookAction.phpSummaryAction.php

    //BookAction.php
    
    declare(strict_types=1);
    
    namespace App\Application\Actions\Book;
    
    use App\Application\Actions\Action;
    use App\Domain\Book\BookRepository;
    
    use Psr\Log\LoggerInterface;
    use Psr\Http\Message\ResponseInterface as Response;
    
    abstract class BookAction extends Action
    {
        /**
         * {@inheritdoc}
         */
        protected $repo;
    
        public function __construct(BookRepository $repo, LoggerInterface $logger) {
            parent::__construct($logger);
            $this->repo=$repo;
        }
    }

    注意到,这是一个抽象类,主要目的是为本类(以及后续子类)引入各种dependency。这里我引入了BookRepositoryLoggerInterface

    //SummaryAction.php
    declare(strict_types=1);
    
    namespace App\Application\Actions\Book;
    
    use Psr\Http\Message\ResponseInterface as Response;
    
    class SummaryAction extends BookAction
    {
        /**
         * {@inheritdoc}
         */
        protected function action(): Response
        {
            $res=$this->repo->summary();
            return $this->respondWithData($res);
        }
    }

    SummaryAction继承了BookAction,完成了action方法。而在action方法中,通过调用repo中对应的方法而获取了数据并返回。Slim4很贴心地可以直接返回json数据,不用开发者再转换。调用的例子如下:

    我们再来看src/Domain目录。

    这个目录中包括用来定义各类仓库的文件。比如我们之前看到的BookRepository

    // /src/Domain/Book/BookRepository.php
    declare(strict_types=1);
    
    namespace App\Domain\Book;
    
    interface BookRepository
    {
        public function summary(): array;
        public function detail($id):array;
    }

    如前所述,这是一个接口,只是定义了各类接口,也就是这个仓库能做什么的定义。

    最后来看看src/Infrastructure/Persistence目录。这个目录对上面提到的接口进行实现。

    // /src/Infrastructure/Persistence/Book/DBBookRepository.php
    declare(strict_types=1);
    
    namespace App\Infrastructure\Persistence\Book;
    
    use App\Domain\Book\BookRepository;
    
    class DBBookRepository implements BookRepository
    {
        private $conn;
    
        public function __construct(\PDO $conn) {
            $this->conn=$conn;
        }
    
        public function summary(): array {
            $sql='select count(*) bc, sum(kword) wc, sum(page) pc from book_book';
            $res=$this->conn->query($sql)->fetch();
    
            return $res;
        }
    
        public function detail($id): array {
    
        }
    
    }

    请注意,这个类的构造函数中引入了\PDO,因此上文提到的dependency就起作用了。

    总体感觉,Slim 4的结构还是很清晰的,耦合度也合适,确实是可以一用的轻量级框架。

    本文推送到[go4pro.org]

  • 重构“任氏有无轩”——第三天

    今天继续加深书籍详细信息页面的构造。

    在G+上95对我的进展发了一个评论:

    再加上点自动抓取网上共享章节的功能

    对这个要求,我只能说我只能实现一点点。我将在详细信息页面中构造一个显示豆瓣对应书籍的信息的部分。

    另外,我要实现一个功能是在这个页面中编辑书籍tags的功能。

    先看实现的界面:

    detail

    先讲在页面中编辑书籍tags。

    这个其实很简单,在控制器中创建一个动作tagaddAction,用来处理“增加更多TAG”按钮的POST动作:

    public function tagaddAction(Request $req)
    {
        $q=$req->request->all();
        $q['newtags'];
        $id=$q['id'];
        $this->processTagAdd($tags, $id);
        $url=$this->get('router')->generate('book_detail', array('id'=>$id));
        return $this->redirect($url);
    }
    private function processTagAdd($tags, $id)
    {
        $em=$this->getDoctrine()-gt;getEntityManager();
        $existing=$em->getRepository('trrsywxBundle:BookBook')->getTagsByBookId($id);
        $existing=$this->convertTagsToArray($existing);
        $book=$em->getRepository('trrsywxBundle:BookBook')->findOneBy(array('id'=>$id));
        $tags=trim($tags);
        $tagslist=  explode(' ', $tags);
        foreach ($tagslist as $tag)
        {
            if(!in_array($tag, $existing)) //This is a new tag
            {
                $booktaglist=new trrsywxBundleEntityBookTaglist();
                $booktaglist->setId($book);
                $booktaglist->setTag($tag);
                $em->persist($booktaglist);
            }
            $em->flush();
        }
    }

    这里有几个需要讲一讲的地方:

    • 在控制器中生成一个url,然后重定向。这也是一般表单post后所需要进行的最后一步。
    • 在processTagAdd中,需要注意$booktaglist->setId()的参数不是$book.id,而是$book本身。所以,虽然我传递进去的是$id,但是之前不得不用一个Repository函数来找出这本书。
    • 注意一个新的taglist是怎样创建,赋值,持续化的。最后,将所有持续化后的对象一次性flush到数据库中去。
    • 用in_array来判断要新加的tag是否已经存在于原来的taglist里,同时要将taglist对象中的tag提取出来变成一个数组——因为它本来是个对象数组。这个是由一个辅助函数convertTagsToArray完成的。

    =====我是分割线=====

    接下来讲一讲获得豆瓣的信息。

    我用到了Snoopy这个库,关于如何在Symfony 2中加入第三方库的说明,参见这篇文章

    获得豆瓣信息的核心代码如下:

    private function getDoubanRemote($isbn)
    {
            $bad_isbn = 'bad isbn';
            $url = "http://api.douban.com/book/subject/isbn/$isbn?alt=json";
            $s = new SnoopySnoopy();
            $s->agent = 'RSYWX.net - http://www.rsywx.net';
            $s->read_timeout = 1;
            //$s->user = 'taylor.ren@gmail.com';
            //$s->pass = 'xxxxxx';
            $s->fetch($url);
            $res = $s->results;
            if ($res == $bad_isbn) // My isbn is not found in douban
                return false;
            else
                return json_decode($res, true);
    }
    public function getDouban($isbn)
    {
            $ret = $this->createDummyReturn();
            $res = $this->getDoubanRemote($isbn);
            if (!$res) // The above call not successful
            {
                return $ret;
            } else
            {
                if (array_key_exists('summary', $res))
                    $summary = $res['summary']['$t'];
                else
                    $summary = '(豆瓣还没有简介)';
                $ret['summary'] = $summary;
                $ret['alternate'] = $res['link'][1]['@href'];
                if (array_key_exists('db:tag', $res))
                    $tags = $res['db:tag'];
                else
                    $tags[] = '豆瓣没有给出任何TAG';
                $ret['tags'] = array(); //Clear up the dummy tag 'not found';
                foreach ($tags as $t)
                {
                    $ret['tags'][] = $t['@name'];
                }
                $rating = $res['gd:rating']['@average'];
                if ($rating == 0)
                    $rating = '(还没有评分)';
                $ret['rating'] = $rating;
            }
            return $ret;
    }

    其实没有什么技术含量,主要是从豆瓣API返回的数组中提取我用得着的数据而已。

    到此,书籍详细页面已经完成。我接下来要做的是书籍列表部分。这个部分需要用到分页。可耻的是,Symfony 2没有内置分页部件,我需要自己来编写。这个会是一个挑战。

    【本文收录于[go4pro.org]】

  • An Android application with map

    I had read a few tutorials on how to develop an Android appication with map display. Today I had actually built one. I would like to summarize the key points/steps in making this application work.

    1. First of all, get an Android Map API Key from Google.

    This actually involves two steps. Firstly, you will have to create a key store to sign your Android application. This is quite easy and straightforward. Secondly, apply an Android Map Key from Google: http://code.google.com/intl/zh-CN/android/maps-api-signup.html. To make this work, you have to know where is your key store file is located (as created in the above step) and also have the JDK tool named keytool. From the command line, type:

       keytool -list -keystore the-path-to-your-key-store-file

    It will prompt you to enter the password to the key store and will generate a MD5 finger print for this particular key store file. Copy this MD5 finger print to the above URI and Google will give you the Android Map API Key immediately. It is strongly suggested to save this Key information.

    1. Create an Android application.

    Note: It must be created with target set to: Google APIs. You should not set the application target to Android x.x or it will not be running properly.

    Note: The target of the AVD that runs the map application must also be set to Google APIs.

    The coding of the application is actually quite simple. There are only two points to be highlighted:

    1. The application must be granted ACCESS_FINE_LOCATION and ACCESS_INTERNET permissions;
    2. The mapview controll used in the view must be provided with the API key generated in Step 1. It will look something like this:
    <com.google.android.maps.mapview android:apikey="your" android:clickable="true" android:enabled="true" android:id="@+id/myMapView" android:layout_height="fill_parent" android:layout_width="fill_parent" api="" here="" key=""></com.google.android.maps.mapview>

    With these settings, the map application can eventually run successfully.

    However, in my implementation, the map shown in my AVD is only grids, no actuall maps at all. But in real machine (mine is Nexus One), the application is running correctly.