Back-End

27 jan, 2014

Building com Phing – Parte 02

Publicidade

Na primeira parte deste artigo vimos o básico sobre como trabalhar com o Phing. Agora vamos ver algumas técnicas para produtividade e funcionalidades mais avançadas, integrando nossa ferramenta de build com outras ferramentas do ecossistema PHP.

Phing, como ferramenta de build, facilita demais nossa vida. Facilita tanto no seu trabalho, como escrevendo suas tarefas. Uma das coisas que nos poupa muito tempo e torna nossas descrições de build mais legíveis e customizáveis é escrever um arquivo de propriedades.

path.settings=/etc/${project.phing.name}
path.tmp=/tmp/${project.phing.name}/${version}
path.deploy.base=/var/lib/projects/${project.phing.name}
path.deploy.version=${path.deploy.base}/${version}
project.bin=${project.basedir}/bin
project.public=${project.basedir}/web
project.app=${project.basedir}/application
project.app.settings=${project.app}/settings
fileset.library=${project.app}/library
fileset.tests=${project.baseddir}/tests
phpunit.settings=${project.basedir}/phpunit.xml
phpcs.standard=PSR2

Aqui temos alguns paths setados, configuração para o phpunit e standard a ser verificado. Vamos adicionar mais um arquivo comum a maioria dos projetos que vemos hoje em dia:

0.5.1

Vamos usar esse VERSION também.  Agora vamos ao nosso build.xml inicial – que será o pontapé do nosso projeto, a fim de explicar diversos pontos não triviais do phing.

<?xml version="1.0" encoding="UTF-8"?>
<project name="myapp" basedir="." default="buildAll">
    <loadfile property="version" file="VERSION"/>
    <property file="build.properties"/>
    <target name="buildAll" depends="retrieve,quality,deploy"/>
    <target name="retrieve">
        <phing target="retrieve-it" phingFile="git.build.xml" inheritRefs="true" haltOnFailure="true"/>
    </target>
    <target name="quality">
        <phing target="check-it" phingFile="quality.build.xml" inheritRefs="true" haltOnFailure="true"/>
    </target>
    <target name="deploy">
        <phing target="deploy-it" phingFile="deploy.build.xml" inheritRefs="true" haltOnFailure="true" />
    </target>
    <target name="undeploy">
        <phing target="undeploy-it" phingFile="deploy.build.xml" inheritRefs="true" haltOnFailure="true"/>
    </target>
</project>

E o que vemos aqui? Na linha 3 estamos lendo o arquivo VERSION na propriedade${version} e lendo o arquivo de propriedades build.properties: todas suas propriedades são globais agora para o build.

Também diferente do artigo anterior não temos nenhum target com tarefas complexas, mas 4 targets com a tarefa phing, chamando um outro arquivo. Por que isso? Se você não esperou este artigo sair e tentou configurar alguma tarefa opcional provavelmente você deve ter se deparado com erros, pois uma extensão ou biblioteca não estava instalada. Se organizamos nosso build.xml dessa maneira, o phing só exigirá determinada dependência no arquivo que ele chamar. Por exemplo: temos na linha 6 o target retrieve, que será responsável por obter o projeto; se as tarefas relacionadas a ele estivessem nesse arquivo (build.xml) e nós não tivéssemos a extensão VersionControl_Git (PEAR) instalada, já teríamos um erro aqui. Mas se quisermos somente executar o target undeploy, que não tem qualquer dependência, o erro não acontece (na verdade acontece, mas vou explicar melhor um pouco mais para frente).

Agora que sabemos o porquê do arquivo principal não ter dependências, vamos ao primeiro de nossa lista:

<?xml version="1.0" encoding="UTF-8"?>
<project name="myapp" basedir="." default="get-it">
    <target name="retrieve-it" depends="get-it,check"/>
    <target name="get-it">
        <gitclone
            repository="git://git.my-serv.er/repos/myapp.git"
            targetPath="${path.tmp}" />
    </target>
    <target name="check">
        <loadfile property="current-version" file="path.deploy.version"/>
        <php function="version_compare" returnProperty="newer">
            <param value="${version}" />
            <param value="${current-version}" />
        </php>
        <if>
            <not><equals arg1="${newer}" arg2="1"/></not>
            <then>
                <fail message="Candidate version [${version}] is older or equal currently installed version"/>
            </then>
        </if>
    </target>
</project>

E o que fazemos aqui? O target get-it clona um repositório git em um diretório temporário. O target check verifica os arquivos VERSION desse clone e de uma possível versão já rodando nesse ambiente e se a versão clonada for anterior ou igual a instalada, ele aborta o processo. Só isso. Onde este build file pode ser útil? Já imaginou uma rotina de deploy realmente automática? Não, você não vai querer fazer isso em produção. Talvez em homologação…

Voltando ao primeiro build.xml, temos um target buildAll, que executa os três seguintes. Se o retrieve-it falhar (por problema ou versão antiga) o quality nem inicia. Mas se iniciar…

<?xml version="1.0" encoding="UTF-8"?>
<project name="myapp" basedir="." default="check-it">
    <fileset dir="application/src" id="quality.source">
        <include name="**/*.php"/>
    </fileset>
    <fileset dir="tests" id="quality.test">
        <include name="**/*.php"/>
    </fileset>
    <target name="check-it" depends="lint,style,unit"/>
    <target name="lint">
        <phplint haltonfailure="true">
            <fileset refid="quality.source"/>
            <fileset refid="quality.test"/>
        </phplint>
    </target>
    <target name="style">
        <phpcodesniffer
            standard="PSR2"
            showSniffs="false"
            showWarnings="true">
            <fileset refid="quality.source"/>
            <formatter type="default" usefile="false"/>
        </phpcodesniffer>
    </style>
    <target name="unit">
        <exec command="phpunit" logoutput="/dev/stdout" checkreturn="true" dir="${project.basedir}"/>
    </target>
</project>

Aí testamos a versão candidata a deploy. Incluí somente alguns testes: lint, checkstyle e Testes Unitários; o Phing permite muitos outros testes, mas a ideia aqui é somente separar a parte de testes. Se repararem, não usei a task phpunit do Phing, mas sim chamando-o via shell. Porque? Alguns motivos: os reports gerados através do phing para coverage não batem com o do phpunit rodando por si só e algumas vezes, aninhando todas as tasks possíveis para phpunit via phing os resultados não são tão bons e/ou nada performáticos. Não se preocupe com o phunit, deixei tudo arrumado para que todos os relatórios sejam gerados (ou não) no phpunit.xml:

<?xml version="1.0" encoding="UTF-8" ?>
<phpunit backupGlobals="true"
         backupStaticAttributes="false"
         bootstrap="./bootstrap.php"
         cacheTokens="false"
         colors="true"
         convertErrorsToExceptions="true"
         convertNoticesToExceptions="true"
         convertWarningsToExceptions="true"
         forceCoversAnnotation="false"
         mapTestClassNameToCoveredClassName="false"
         processIsolation="false"
         stopOnError="false"
         stopOnFailure="false"
         stopOnIncomplete="false"
         stopOnSkipped="false"
         strict="false"
         verbose="true">
    <testsuites>
        <testsuite name="Some Project Test Suite">
            <directory suffix="Test.php" phpVersion="5.4.0" phpVersionOperator=">=">./tests</directory>
        </testsuite>
    </testsuites>
    <!--logging>
        <log type="coverage-html" target="./report/coverage" charset="UTF-8"
             highlight="true" lowUpperBound="35" highLowerBound="70" showUncoveredFiles="true"/>
        <log type="coverage-clover" target="./report/coverage.xml"/>
        <log type="coverage-php" target="/tmp/coverage.serialized"/>
        <log type="coverage-text" target="php://stdout" showUncoveredFiles="false"/>
        <log type="json" target="./report/logfile.json"/>
        <log type="tap" target="./report/logfile.tap"/>
        <log type="junit" target="./report/logfile.xml" logIncompleteSkipped="false"/>
        <log type="testdox-html" target="./report/testdox.html"/>
    </logging-->
    <filter>
        <whitelist processUncoveredFilesFromWhitelist="true">
            <directory suffix=".php">library</directory>
            <exclude>
                <directory suffix=".php">tests</directory>
            </exclude>
        </whitelist>
    </filter>
</phpunit>

Simples não? O importante aqui é que se algo não estiver bem, como um erro de sintaxe, o processo não continua. E mais: vocês não precisam ficar presos ao que o Phing dispõe, uma vez que pode invocar um script já existente (como fiz agora com o phpunit) ou mesmo criar extensões para ele, no próprio build.xml ou criando classes no seu projeto – mas isso é assunto para outro dia.

Bom, para finalizar, fica aqui a parte do build que vai de fato fazer o “deploy” de fato:

<?xml version="1.0" encoding="UTF-8"?>
    <project name="myapp" basedir="." default="deploy-it">
        <fileset dir="application" id="deploy.source">
            <include name="library/**"/>
            <include name="web/**"/>
            <include name="bin/**"/>
            <include name="resources/**"/>
            <include name="settings/**"/>
        </fileset>
        <target name="deploy-it" depends="copy,symlink,refresh"/>
        <target name="copy">
            <mkdir dir="${path.deploy.version}"/>
            <copy todir="${path.deploy.version}">
                <fileset refid="deploy.source"/>
            </copy>
        </target>
        <target name="symlink">
            <delete file="${path.deploy.base}/previous"/>
            <exec command="mv ${path.deploy.base}/current ${path.deploy.base}/previous"/>
            <exec command="ln -s ${path.deploy.version} ${path.deploy.base}/current"/>
        </target>
        <target name="refresh">
            <exec command="service php5-fpm restart"/>
        </target>
        <target name="undeploy-it">
            <delete file="${path.deploy.base}/current"/>
            <exec command="ln -s ${path.deploy.base}/previous ${path.deploy.base}/current"/>
            <exec command="service php5-fpm restart"/>
        </target>
    </project>

Que é descrito por 3 targets: copysymlink e refresh. No primeiro copiamos a aplicação para um diretório específica para a versão que estamos colocando no ar. Só como exemplo, a listagem deste diretório seria mais ou menos assim:

Listagem para /var/lib/projects/my-project0.4.0
0.4.1
0.4.2
0.5.0
0.5.1
current -> /var/lib/projects/my-project/0.5.0

O target symlink vai remover o current e recriá-lo como 0.5.1, nossa versão canditada. Já o target refresh vai reinicializar o FPM. Se algo der errado apesar de todos esses cuidados, podemos fazer o rollback, descrito no targetundeploy.

Considerações: Estes arquivos de build não devem ser levados a risca para a execução de um deploy – a intenção foi apresentar-lhes a descrição de build em multiplos arquivos e algumas tarefas opcionais do phing com um propósito definido. Para saber mais sobre Phing acesse o site e dê uma olhada no manual: [Phing]

O Build é uma parte essencial no desenvolvimento de software – ele garante que processos de muitos passos sejam executados sempre da mesma maneira, sem a possibilidade do “esqueci de rodar o phpunit, mas foi só dessa vez”. Essa automação do processo propicia Agilidade e Qualidade no seu Projeto.

Já usa processo de build no seu desenvolvimento? Com Phing ou outra ferramenta? Curtiu? Não? Deixe sua dúvida, comentário, sugestão ou reclamação.