Programando para programadores 
Desafios na evolução de um Framework 
Pablo Dall'Oglio 
@pablodalloglio fb/pablodalloglio
Meu caminho 
● Clipper (1994-1998): comercial, bibliotecas, funções; 
● Delphi (1998-1999): automação, componentes; 
● PHP (2000): SAGU (php+html+sql); 
● PHP-GTK(2001): PHP só com classes; 
● Agata Report (2001-2006); 
● Gnuteca (2002): PHP Web com classes; 
● PHP-GTK: Criando Aplicações Gráficas com PHP (2004); 
● Design Patterns (2004): Unisinos; 
● Core (2006): Primeira experiência com Framework MVC; 
● PHP: Programando com Orientação a Objetos (2007); 
● Mestrado em Engenharia de Software (2008, 2009); 
● Criando Relatórios com PHP (2011); 
● Adianti Framework para PHP (2012). 
Adianti Solutions Ltda © Pablo Dall'Oglio Programando para Programadores #2
Introdução 
● Software como aplicação (final): 
– Atende os requisitos? Funciona bem? 
– Como é a usabilidade? Como é o desempenho? 
● Software como framework (meio): 
– Segue o padrão de código X? O de arquitetura Y? 
– Como é a interoperabilidade? E o aprendizado? 
● Desafios: 
– Atender padrões globais; 
– Facilidade no aprendizado; 
– Evolução tecnológica. 
Vamos compartilhar 
algumas 
experiências 
Adianti Solutions Ltda © Pablo Dall'Oglio Programando para Programadores #3
Framework 
Experiências com o Adianti Framework: 
● Primeira geração em 2006; 
● Versão atual iniciou ~2008; 
● Lançado oficialmente em 2012; 
● Focado em Business apps. 
Adianti Solutions Ltda © Pablo Dall'Oglio Programando para Programadores #4
#Go Horse 1 
<?php 
// configuração 
$conn = pg_connect("host=localhost port=5432 dbname=exemplos..."); 
// query 
$query = 'SELECT id, nome, endereco FROM cliente WHERE id not in (...)'; 
// resultados 
$result = pg_query($conn, $query); 
if ($result) 
{ 
Apresentação 
// apresentação 
echo '<table border="1">'; 
while ($row = pg_fetch_assoc($result)) 
{ 
echo '<tr>'; 
echo '<td>' . $row['id'] . '</td>'; 
echo '<td>' . $row['nome'] . '</td>'; 
echo '<td>' . $row['endereco'] . '</td>'; 
echo '</tr>'; 
} 
echo '</table>'; 
} 
pg_close($conn); 
Configuração 
Business rule? 
Adianti Solutions Ltda © Pablo Dall'Oglio Programando para Programadores #5
Componentes 
Componentes de interface. 
$this->datagrid = new TDataGrid; 
$code = new TDataGridColumn('code', ...); 
$name = new TDataGridColumn('name', ...); 
$this->datagrid->addColumn($code); 
$this->datagrid->addColumn($name); 
$act1 = new TDataGridAction(array($this, 'onView')); 
$act1->setLabel('View name'); 
$act1->setImage('bs:search blue'); 
$act1->setField('name'); 
$act_group = new TDataGridActionGroup('Actions', 'bs:th'); 
$act_group->addHeader('Available Options'); 
$act_group->addAction($act1); 
$act_group->addAction($act2); 
$this->datagrid->addActionGroup($act_group); 
Abstrair a tecnologia 
Solução em alto nível 
Adianti Solutions Ltda © Pablo Dall'Oglio Programando para Programadores #6
Componentes 
Seleção de valores 
class FormDBAutoSelectionView ... 
{ 
private $form; 
function __construct() 
{ 
parent::__construct(); 
$this->form = new TForm; 
$notebook->appendPage('Automatic selection', $this->form); 
$radio = new TDBRadioGroup('radio','samples','Category', ...); 
$check = new TDBCheckGroup('check','samples','Category',...); 
$combo = new TDBCombo('combo', 'samples', 'Category', ...); 
$select= new TDBSelect('select', 'samples', 'Category', ...); 
$search= new TDBMultiSearch('search','samples','Category', ...); 
$search->setMinLength(3); 
Adianti Solutions Ltda © Pablo Dall'Oglio Programando para Programadores #7
Melhorias estruturais 
1.0.0 própria 
1.0.1 jQuery 
1.0.3 bootstrap 2 
Geração 1: Bootstrap 2 
Geração 2: Bootstrap 3 
class TMessage 
{ 
private $id; 
private $action; 
public function __construct($type, ...) 
{ 
$this->id = uniqid(); 
$modal_wrapper = new TElement('div'); 
$modal_wrapper->{'class'} = 'modal'; 
$modal_wrapper->{'id'} = $this->id; 
$modal_dialog = new TElement('div'); 
$modal_dialog->{'class'} = 'modal-dialog'; 
$modal_content = new TElement('div'); 
$modal_content->{'class'} = 'modal-content'; 
Adianti Solutions Ltda © Pablo Dall'Oglio Programando para Programadores #8
Componentes 
Estrutura de um componente 
class TAccordion extends TElement 
{ 
public function __construct() 
{ 
parent::__construct('div'); 
$this->id = 'taccordion_' . uniqid(); 
} 
public function appendPage($title, $object) 
public function outroMetodo($title, $object) 
public function show() 
{ 
$title = new TElement('h3'); 
$content = new TElement('div'); 
parent::add($title); 
parent::add($content); 
parent::show(); 
} 
Adianti Solutions Ltda © Pablo Dall'Oglio Programando para Programadores #9 
} 
Estrutura para 
Extensibilidade 
app/lib
Estilização 
Customização de estilo: 
$this->datagrid = new TQuickGrid; 
$this->datagrid->class = 'customized-table'; 
parent::include_css('/.../include/custom-table.css'); 
$this->form = new TQuickForm; 
$this->form->style = 'width: 500px'; 
$this->form->class = 'tform'; 
$bt = new TButton('bt'); 
$bt->setLabel('Warning'); 
$bt->class = 'btn btn-warning btn-sm'; 
Descolamento 
entre dados, 
estrutura 
e estilização 
Adianti Solutions Ltda © Pablo Dall'Oglio Programando para Programadores #10
Templates 
Interfaces muito específicas (menor reuso): 
Adianti Solutions Ltda © Pablo Dall'Oglio Programando para Programadores #11
Templates 
Templates próprios 
class ProductCatalogView extends TPage 
{ 
public function __construct() 
{ 
Estrutura 
parent::__construct(); 
$this->html = new THtmlRenderer('app/.../catalog.html'); 
TPage::include_css('app/resources/catalog.css'); 
Estilo 
$replace = array(); 
$this->html->enableSection('main', $replace); 
$replace_detail[] = $product->toArray(); 
$replace_detail[] = $product->toArray(); 
$this->html->enableSection('products', $replace_detail, TRUE); 
parent::add($this->html); 
} 
Conteúdo 
Adianti Solutions Ltda © Pablo Dall'Oglio Programando para Programadores #12
Desafios da evolução 
● Aderência à padrões da comunidade (Namespaces); 
● Acrescentar novos componentes de Interface; 
● Melhorar componentes existentes (+features); 
● Melhorar visual padrão, permitir customização; 
● Melhorar a estrutura de diretórios; 
● Substituir tecnologia inerente (jQuery, Bootstrap); 
● Agregar tecnologia (Font Awesome); 
● Tudo continuar funcionando; 
● Projetos grandes sendo desenvolvidos. 
Adianti Solutions Ltda © Pablo Dall'Oglio Programando para Programadores #13
Estrutura de diretórios 
Isolamento entre app (user) e lib (framework, third parts). 
A carga das classes do 
FW é com ele mesmo. 
Onde geralmente 
ocorrem as mudanças 
globais em migrações 
Adianti Solutions Ltda © Pablo Dall'Oglio Programando para Programadores #14
Aderência a padrões 
Namespaces 
Utilização de class_alias() para compatibilidade 
Adianti Solutions Ltda © Pablo Dall'Oglio Programando para Programadores #15
Novos componentes 
● Release 1.0.1 
– TSpinner, TSlider 
● Release 1.0.2 
– TTreeview 
● Release 1.0.3 
– TSortList 
– TSelect 
● Release 2.0 
Sem 
Traumas! 
– TMultiSearch, T...ActionGroup 
– TExpander, TInputDialog, TColor 
Adianti Solutions Ltda © Pablo Dall'Oglio Programando para Programadores #16
Novas features 
● Release 1.0.1 
– Edição inline (jquery.editinplace.js) 
– setExitAction (+ método) 
● Release 1.0.2 
Migration: 
Acréscimos, 
substituições 
em /lib 
– TEntry::setNumericMask (jquery.maskedinput.js) 
● Release 1.0.3 
– Nova TFile (TFile.php, tfile.js, tfile.css, same interface); 
● Release 2.0 
– Datagrid popover (jquery → bootstrap, libraries.html) 
Adianti Solutions Ltda © Pablo Dall'Oglio Programando para Programadores #17
Bibliotecas utilizadas 
<!-- Third part libraries required by Adianti Framework --> 
<script src="lib/jquery/jquery.min.js"></script> 
<script src="lib/bootstrap/js/bootstrap.min.js"></script> 
<script src="lib/bootstrap/js/bootstrap-colorpicker.min.js"></script> 
<script src="lib/jquery/jquery-ui.min.js"></script> 
<script src="lib/jquery/jquery.blockUI.min.js"></script> 
<!-- Adianti Framework core and components --> 
<script src="lib/adianti/include/adianti.min.js"></script> 
<script src="lib/adianti/include/components.min.js"></script> 
<!-- Application custom Javascript (Optional) --> 
<script src="app/lib/include/application.js"></script> 
<!-- Third part CSS required by Adianti Framework --> 
<link href="lib/jquery/jquery-ui.min.css"/> 
<link href="lib/bootstrap/css/bootstrap.min.css"/> 
<link href="lib/font-awesome/css/font-awesome.min.css"/> 
<!-- Adianti Framework Components CSS --> 
<link href="lib/adianti/include/adianti.min.css"/> 
<link href="lib/adianti/include/components.css"/> 
<!-- Application custom CSS --> 
<link href="app/templates/{template}/application.css"/> 
Bootstrap 
Dialogs, buttons, 
dropdown, menubar, 
tooltip, color picker, 
date picker, multisearch 
libraries.html 
Adianti Solutions Ltda © Pablo Dall'Oglio Programando para Programadores #18
#Go Horse 2 
class ContaReceber 
{ 
function inserir($id, $a, $b, $c) { 
$sql = "INSERT INTO conta_receber..."; 
// exec $sql; 
} 
function listar() { 
$sql = "SELECT * FROM conta_receber"; 
// exec $sql 
} 
function getContasEmAberto() { 
$sql = "SELECT id, sum(valor) 
Começou a separar 
FROM contas_receber cr, lancamentos l 
WHERE l.conta_id = cr.id 
GROUP BY 1 HAVING sum(valor) >1”; 
} 
function getContasEmAtraso() { 
$sql = "SELECT ... 
... 
WHERE dt_vencimento <= date(now())"; 
} 
Adianti Solutions Ltda © Pablo Dall'Oglio Programando para Programadores #19 
} 
Lógicas complexas 
extensas em SQL 
Código reflexo do BD
#Go Horse SP 
DROP PROCEDURE IF EXISTS `VALIDAVENDA`; 
CREATE PROCEDURE `VALIDAVENDA`(IN ICODVENDA int(11), OUT CERRO VARCHAR(60)) 
BEGIN 
SET @CCREDITO = (select vendas.cod_venda from vendas where vendas.cod_venda 
= ICODVENDA and vendas.status = 1); 
SET @PRODUTO = (select vendas.cod_prod from vendas where vendas.cod_venda = 
ICODVENDA and vendas.status = 0); 
SET @COD_TITULO = (select contas_receber.cod_titulo from contas_receber 
where contas_receber.cod_venda = ICODVENDA); 
SELECT IF(@CCREDITO IS NULL,'OK','Já existe credito para a venda referente 
a devolução!') INTO CERRO; 
IF CERRO = 'OK' THEN 
UPDATE PRODUTO SET PRODUTO.FLAG_VENDA = "N" WHERE PRODUTO.COD_PROD = 
@PRODUTO; 
UPDATE CONTAS_RECEBER SET CONTAS_RECEBER.STATUS = "Cancelado" 
WHERE CONTAS_RECEBER.COD_VENDA = ICODVENDA; 
WHILE @COD_TITULO IS NOT NULL DO 
DELETE FROM CR_CAIXA WHERE CR_CAIXA.COD_TITULO = @COD_TITULO; 
END WHILE; 
END IF; 
Como chamo aquela SP do formulário? 
END; 
Adianti Solutions Ltda © Pablo Dall'Oglio Programando para Programadores #20
Orientação a modelos 
Aplicação orientada à SQL (BAD) 
● Inserir autor em livro 
INSERT INTO autor_livro (autor_id, livro_id) 
VALUES ('$autor_id', '$livro_id'); 
● Retornar os itens da Nota Fiscal 
SELECT * FROM nf, itens 
WHERE nf.id = itens.nf_id AND nf.id = '$nf_id' 
● Percorrer dados relacionados 
SELECT * FROM turma, matricula, aluno 
WHERE turma.id=matricula.turma_id 
AND matricula.id_aluno = aluno.id 
AND turma.id = '$turma_id' 
Cérebro orientado 
à relações simples 
chaves entre tabelas 
Ver 10 piores SQL 
Adianti Solutions Ltda © Pablo Dall'Oglio Programando para Programadores #21
Orientação a modelos 
Aplicação orientada ao domínio (GOOD) 
XMI → {PHP, SQL} 
Utilização de 
relações 
mais complexas. 
Modelo relacional 
é consequência 
Adianti Solutions Ltda © Pablo Dall'Oglio Programando para Programadores #22
Orientação a modelos 
Exemplo de implementação 
class Filme extends TRecord 
{ 
public function get_genero() { 
if (empty($this->genero)) { 
Lazy Load 
$this->genero = new Genero( $this->genero_id ); 
} 
return $this->genero; 
} 
public function addAtor( Ator $object ) { 
$this->atores[] = $object; 
} 
public function getAtores() { 
return $this->atores; 
} 
public function load($id) { 
$this->criticas = parent::loadComposite('Critica', ...); 
$this->atores = parent::loadAggregate('Ator', ...); 
return parent::load($id); 
} 
Adianti Solutions Ltda © Pablo Dall'Oglio Programando para Programadores #23 
} 
Relações mais 
complexas, é 
preciso treinar... 
Métodos auxiliares
Orientação a modelos 
Exemplo de utilização 
// carrega o filme 
$filme = new Filme(5); 
print $filme->distribuidor->nome; 
print $filme->genero->nome; 
// carrega o filme 
$filme = new Filme(5); 
foreach ($filme->getAtores() as $ator) 
{ 
print $ator->nome; 
Adianti Solutions Ltda © Pablo Dall'Oglio Programando para Programadores #24 
} 
// adiciona o ator 
$ator = new Ator(10); 
$filme->addAtor( $ator ); 
$filme->store(); 
LEGIBILIDADE 
Lazy Load MAIOR 
Agregação
Orientação a modelos 
Aplicação orientada ao domínio (GOOD) 
● Inserir autor em livro 
$livro = new Livro( 10 ); 
$livro->addAutor( new Autor(8) ); 
$livro->store(); 
● Retornar os itens da Nota Fiscal 
$nf = new NotaFiscal(10); 
foreach ($nf->getItems() as $item) { 
print $item->produto->descricao; 
} 
● Percorrer dados relacionados 
NAVEGABILIDADE 
entre relações 
$turma = new Turma(10); 
foreach ($turma->getMatriculas() as $matricula) { 
print $matricula->aluno->nome; 
} 
Adianti Solutions Ltda © Pablo Dall'Oglio Programando para Programadores #25
Persistência 
Armazenar um novo objeto (INSERT). 
//... 
TTransaction::open('samples'); // abre uma transação 
$object = new Pessoa; 
$object->name = 'Maria da Silva'; 
$object->address = 'Rua da Conceicao'; 
$object->phone = '(51) 8111-2222'; 
$object->birthdate = '2013-02-15'; 
$object->status = 'S'; 
$object->email = 'maria@email.com'; 
$object->gender = 'M'; 
$object->store(); // armazena o objeto 
TTransaction::close(); // fecha a transação. 
//... 
Adianti Solutions Ltda © Pablo Dall'Oglio Programando para Programadores #26
Persistência 
Alterar um objeto já existente (UPDATE). 
//... 
TTransaction::open('samples'); // abre uma transação 
$objeto = new Customer; // instancia o cliente 
$customer= $objeto->load(31); // carrega o cliente 31 
if ($customer) // se existe 
{ 
$customer->phone = '51 8111-3333'; // muda o fone 
$customer->store(); // armazena o objeto 
} 
new TMessage('info', 'Objeto atualizado'); 
TTransaction::close(); // fecha a transação. 
//... 
Adianti Solutions Ltda © Pablo Dall'Oglio Programando para Programadores #27
Persistência 
Manipular um conjunto de objetos conforme um filtro. 
//... 
TTransaction::open('samples'); // abre uma transação 
$repository = new TRepository('Customer'); 
$customers = $repository->where('gender', '=', 'M') 
->where('name', 'like', 'A%') 
->load(); 
foreach ($customers as $customer) 
{ 
$customer->phone = '84 '.substr($customer->phone, 3); 
$customer->store(); 
} 
Active Record 
TTransaction::close(); // fecha a transação. 
//... 
Repository 
Pattern 
Adianti Solutions Ltda © Pablo Dall'Oglio Programando para Programadores #28
Object caching 
Melhorias para a versão 2.0: 
● Cache Service 
class Customer extends TRecord 
{ 
1/3 tempo na matrícula 
const TABLENAME = 'customer'; 
const PRIMARYKEY = 'id'; 
const IDPOLICY = 'max'; // {max, serial} 
const CACHECONTROL = 'Cache-Control-Class'; 
// ... 
} 
● Utilizado em: 
Grandes ganhos de 
Performance 
Deve implementar 
classe específica 
– Carregar objeto, Salvar objeto, Excluir objeto; 
– Carregar coleções, Excluir coleções; 
Adianti Solutions Ltda © Pablo Dall'Oglio Programando para Programadores #29
Object caching 
Implementação de cache usando APC: 
class TAPCache implements AdiantiRegistryInterface 
{ 
public static function enabled() { 
return extension_loaded('apc'); 
} 
public static function setValue($key, $value) { 
return apc_store($key, serialize($value)); 
} 
public static function getValue($key) { 
return unserialize(apc_fetch($key)); 
} 
public static function delValue($key) { 
return apc_delete($key); 
} 
} 
Adianti Solutions Ltda © Pablo Dall'Oglio Programando para Programadores #30
Prepared Statements 
Melhorias para a versão 2.0: 
● Prepared Statements transparente: 
– Habilitar/desabilitar por transação (performance); 
$data = $this->form->getData(); 
Query Object pattern 
TTransaction::open('samples'); 
$criteria = new TCriteria; 
$criteria->add(new TFilter('gender', '=', $data->gender)); 
$criteria->add(new TFilter('age', '>', $data->age)); 
$criteria->add(new TFilter('haircolor', '>', $data->haircolor)); 
$repository = new TRepository('Customer'); 
$customers = $repository->load($criteria); 
TTransaction::close(); 
Prepared 
Adianti Solutions Ltda © Pablo Dall'Oglio Programando para Programadores #31
Prepared Statements 
Como o Prepared é processado: 
TTransaction::log($sql->getInstruction()); 
$dbinfo = TTransaction::getDatabaseInfo(); 
if (isset($dbinfo['prep']) AND $dbinfo['prep'] == '1') 
{ 
Com prepared 
$result = $conn->prepare($sql->getInstruction(TRUE), ...); 
$result-> execute( $sql->getPreparedVars() ); 
} 
else { 
Sem prepared 
$result = $conn-> query($sql->getInstruction()); 
} 
if ( $cache = $this->getCacheControl() ) { 
Atualiza cache 
$record_key = $class . '['. $this->$pk . ']'; 
if ($cache::setValue( $record_key, $this->toArray() )) { 
TTransaction::log($record_key . ' stored in cache'); 
} 
} 
return $result; 
Adianti Solutions Ltda © Pablo Dall'Oglio Programando para Programadores #32
Novas restrições 
● Release 1.0.2 
– TForm::addField() 2 campos mesmo nome; 
– TForm::addField() só aceita AdiantiWidgetInterface. 
class TForm 
{ 
public function addField( AdiantiWidgetInterface $field ) 
{ 
$name = $field->getName(); 
if (isset($this->fields[$name])) 
{ 
throw new Exception(TAdiantiCoreTranslator::translate('You 
have already added a field called "^1" inside the form', $name)); 
} 
$this->fields[$name] = $field; 
$field->setFormName($this->name); 
} 
Adianti Solutions Ltda © Pablo Dall'Oglio Programando para Programadores #33 
} 
Resiliência 
1o programador = instrução 
2o programador = exceção
Novas restrições 
● Release 1.0.3 
– TRecord::addAttribute() não pode ter atributo data; 
– setLogger() sem transação ativa lança exceção; 
final class TTransaction 
{ 
public static function setLogger( TLogger $logger) 
{ 
if (isset(self::$conn[self::$counter])) 
{ 
self::$logger[self::$counter] = $logger; 
} 
else 
{ 
Não esperar que 
as coisas 
estejam certas 
throw new Exception(TAdiantiCoreTranslator::translate('No 
active transactions') . ': ' . __METHOD__); 
} 
} 
Adianti Solutions Ltda © Pablo Dall'Oglio Programando para Programadores #34 
} 
Controle
Novas restrições 
● Release 2.0 (PSR-3) 
– TTransaction implements LoggerAwareInterface; 
– TLogger's implements LoggerInterface. 
final class TTransaction implements LoggerAwareInterface 
{ 
Ciência 
public static function setLogger( LoggerInterface $logger) 
{ 
if (isset(self::$conn[self::$counter])) 
{ 
self::$logger[self::$counter] = $logger; 
} 
else 
{ 
throw new Exception(TAdiantiCoreTranslator::translate('No 
active transactions') . ': ' . __METHOD__); 
} 
} 
Adianti Solutions Ltda © Pablo Dall'Oglio Programando para Programadores #35 
} 
Antes, TLogger
E o que mais? 
● Práticas! 
– Onde colocar tal método? M,V, ou C? Qual das M? 
– Nunca a Model deve dar TMessage; 
– Model não deve dar return em falha, só throw; 
– Nunca abrir transaction na model (Model agnóstica); 
– Catch sempre na controller; 
– Transactions somente na controller; 
– Usar getByAlgumaCoisa(). Ex: Turma::getByCodigo(); 
– Objetos de negócio sempre em Português (Aluno); 
– Métodos-ação imprimeAluno(), geraCertificado(). 
Adianti Solutions Ltda © Pablo Dall'Oglio Programando para Programadores #36
Ferramentas profissionais
Studio Pro 
<?php 
/** 
* Customer Active Record 
* @author <your-name-here> 
*/ 
class Customer extends TRecord 
{ 
public function get_city() 
public function addSkill(Skill $skill) 
public function getSkills() 
public function load($id) 
public function store() 
public function delete($id = NULL) 
Adianti Solutions Ltda © Pablo Dall'Oglio Programando para Programadores #38 
} ?> 
XMI 
SQL 
PHP 
Modelo 
Astah 
StarUML
Studio Pro 
● Se o modelo for feito antes, facilita tudo; 
● Relações podem ser definidas por wizards também. 
<?php 
// load customer 
$obj = new Customer(5); 
print $obj->city->name; 
print $obj->category->name; 
// percorre contatos 
foreach ($obj->getContacts() as $contact) 
{ 
$contact->value = '9'.$contact->value; 
$contact->store(); 
print $contact->type . '-' . $contact->value; 
} 
foreach ($obj->getSkills() as $skill) 
{ 
print $skill->name; 
} 
Adianti Solutions Ltda © Pablo Dall'Oglio Programando para Programadores #39
Studio Form Designer 
Adianti Solutions Ltda © Pablo Dall'Oglio Programando para Programadores #40
Studio Form Designer 
class TestView extends TPage 
{ 
private $form; 
function __construct() 
{ 
parent::__construct(); 
$this->form = new TForm; 
try { 
Wrapper 
$ui = new TUIBuilder(500,300); 
$ui->setController($this); 
$ui->setForm($this->form); 
$ui->parseFile('app/forms/sample.form.xml'); 
$this->form->add($ui); 
$this->form->setFields($ui->getFields()); 
} 
catch (Exception $e) { 
new TMessage('error', $e->getMessage()); 
} 
parent::add($this->form); 
Adianti Solutions Ltda © Pablo Dall'Oglio Programando para Programadores #41
Studio PDF Designer 
Adianti Solutions Ltda © Pablo Dall'Oglio Programando para Programadores #42
Studio PDF Designer 
class PDFDesignNFEView extends TPage 
{ 
function onGenerate() 
{ 
Wrapper 
try 
{ 
$designer = new TPDFDesigner; 
$designer->fromXml('app/reports/nfe.pdf.xml'); 
$designer->generate(); 
$designer->SetFont('Arial', 'B', 8); 
$designer->setFontColorRGB( '#4C4491' ); 
$designer->writeAtAnchor('bairro', 'Centro'); 
$designer->writeAtAnchor('municipio', 'Cidade teste'); 
$designer->writeAtAnchor('fone', '(11) 1234-5678'); 
$designer->gotoAnchorXY('details'); 
$designer->SetFont('Arial', '', 8); 
$designer->Cell( 62, 10, '12121212', 1, 0, 'C'); 
$designer->Cell(140, 10, utf8_decode('Guaraná'), 1, 0, 'L'); 
} 
//... 
Adianti Solutions Ltda © Pablo Dall'Oglio Programando para Programadores #43
Obrigado 
● Adianti Framework: 
– www.adianti.com.br/framework 
● Contato: 
– www.dalloglio.net 
– www.adianti.com.br 
– @pablodalloglio 
– @adiantisolution 
● Não esquecer de falar do Sorteio! 
Adianti Solutions Ltda © Pablo Dall'Oglio Programando para Programadores #44

Programando para programadores: Desafios na evolução de um Framework

  • 1.
    Programando para programadores Desafios na evolução de um Framework Pablo Dall'Oglio @pablodalloglio fb/pablodalloglio
  • 2.
    Meu caminho ●Clipper (1994-1998): comercial, bibliotecas, funções; ● Delphi (1998-1999): automação, componentes; ● PHP (2000): SAGU (php+html+sql); ● PHP-GTK(2001): PHP só com classes; ● Agata Report (2001-2006); ● Gnuteca (2002): PHP Web com classes; ● PHP-GTK: Criando Aplicações Gráficas com PHP (2004); ● Design Patterns (2004): Unisinos; ● Core (2006): Primeira experiência com Framework MVC; ● PHP: Programando com Orientação a Objetos (2007); ● Mestrado em Engenharia de Software (2008, 2009); ● Criando Relatórios com PHP (2011); ● Adianti Framework para PHP (2012). Adianti Solutions Ltda © Pablo Dall'Oglio Programando para Programadores #2
  • 3.
    Introdução ● Softwarecomo aplicação (final): – Atende os requisitos? Funciona bem? – Como é a usabilidade? Como é o desempenho? ● Software como framework (meio): – Segue o padrão de código X? O de arquitetura Y? – Como é a interoperabilidade? E o aprendizado? ● Desafios: – Atender padrões globais; – Facilidade no aprendizado; – Evolução tecnológica. Vamos compartilhar algumas experiências Adianti Solutions Ltda © Pablo Dall'Oglio Programando para Programadores #3
  • 4.
    Framework Experiências como Adianti Framework: ● Primeira geração em 2006; ● Versão atual iniciou ~2008; ● Lançado oficialmente em 2012; ● Focado em Business apps. Adianti Solutions Ltda © Pablo Dall'Oglio Programando para Programadores #4
  • 5.
    #Go Horse 1 <?php // configuração $conn = pg_connect("host=localhost port=5432 dbname=exemplos..."); // query $query = 'SELECT id, nome, endereco FROM cliente WHERE id not in (...)'; // resultados $result = pg_query($conn, $query); if ($result) { Apresentação // apresentação echo '<table border="1">'; while ($row = pg_fetch_assoc($result)) { echo '<tr>'; echo '<td>' . $row['id'] . '</td>'; echo '<td>' . $row['nome'] . '</td>'; echo '<td>' . $row['endereco'] . '</td>'; echo '</tr>'; } echo '</table>'; } pg_close($conn); Configuração Business rule? Adianti Solutions Ltda © Pablo Dall'Oglio Programando para Programadores #5
  • 6.
    Componentes Componentes deinterface. $this->datagrid = new TDataGrid; $code = new TDataGridColumn('code', ...); $name = new TDataGridColumn('name', ...); $this->datagrid->addColumn($code); $this->datagrid->addColumn($name); $act1 = new TDataGridAction(array($this, 'onView')); $act1->setLabel('View name'); $act1->setImage('bs:search blue'); $act1->setField('name'); $act_group = new TDataGridActionGroup('Actions', 'bs:th'); $act_group->addHeader('Available Options'); $act_group->addAction($act1); $act_group->addAction($act2); $this->datagrid->addActionGroup($act_group); Abstrair a tecnologia Solução em alto nível Adianti Solutions Ltda © Pablo Dall'Oglio Programando para Programadores #6
  • 7.
    Componentes Seleção devalores class FormDBAutoSelectionView ... { private $form; function __construct() { parent::__construct(); $this->form = new TForm; $notebook->appendPage('Automatic selection', $this->form); $radio = new TDBRadioGroup('radio','samples','Category', ...); $check = new TDBCheckGroup('check','samples','Category',...); $combo = new TDBCombo('combo', 'samples', 'Category', ...); $select= new TDBSelect('select', 'samples', 'Category', ...); $search= new TDBMultiSearch('search','samples','Category', ...); $search->setMinLength(3); Adianti Solutions Ltda © Pablo Dall'Oglio Programando para Programadores #7
  • 8.
    Melhorias estruturais 1.0.0própria 1.0.1 jQuery 1.0.3 bootstrap 2 Geração 1: Bootstrap 2 Geração 2: Bootstrap 3 class TMessage { private $id; private $action; public function __construct($type, ...) { $this->id = uniqid(); $modal_wrapper = new TElement('div'); $modal_wrapper->{'class'} = 'modal'; $modal_wrapper->{'id'} = $this->id; $modal_dialog = new TElement('div'); $modal_dialog->{'class'} = 'modal-dialog'; $modal_content = new TElement('div'); $modal_content->{'class'} = 'modal-content'; Adianti Solutions Ltda © Pablo Dall'Oglio Programando para Programadores #8
  • 9.
    Componentes Estrutura deum componente class TAccordion extends TElement { public function __construct() { parent::__construct('div'); $this->id = 'taccordion_' . uniqid(); } public function appendPage($title, $object) public function outroMetodo($title, $object) public function show() { $title = new TElement('h3'); $content = new TElement('div'); parent::add($title); parent::add($content); parent::show(); } Adianti Solutions Ltda © Pablo Dall'Oglio Programando para Programadores #9 } Estrutura para Extensibilidade app/lib
  • 10.
    Estilização Customização deestilo: $this->datagrid = new TQuickGrid; $this->datagrid->class = 'customized-table'; parent::include_css('/.../include/custom-table.css'); $this->form = new TQuickForm; $this->form->style = 'width: 500px'; $this->form->class = 'tform'; $bt = new TButton('bt'); $bt->setLabel('Warning'); $bt->class = 'btn btn-warning btn-sm'; Descolamento entre dados, estrutura e estilização Adianti Solutions Ltda © Pablo Dall'Oglio Programando para Programadores #10
  • 11.
    Templates Interfaces muitoespecíficas (menor reuso): Adianti Solutions Ltda © Pablo Dall'Oglio Programando para Programadores #11
  • 12.
    Templates Templates próprios class ProductCatalogView extends TPage { public function __construct() { Estrutura parent::__construct(); $this->html = new THtmlRenderer('app/.../catalog.html'); TPage::include_css('app/resources/catalog.css'); Estilo $replace = array(); $this->html->enableSection('main', $replace); $replace_detail[] = $product->toArray(); $replace_detail[] = $product->toArray(); $this->html->enableSection('products', $replace_detail, TRUE); parent::add($this->html); } Conteúdo Adianti Solutions Ltda © Pablo Dall'Oglio Programando para Programadores #12
  • 13.
    Desafios da evolução ● Aderência à padrões da comunidade (Namespaces); ● Acrescentar novos componentes de Interface; ● Melhorar componentes existentes (+features); ● Melhorar visual padrão, permitir customização; ● Melhorar a estrutura de diretórios; ● Substituir tecnologia inerente (jQuery, Bootstrap); ● Agregar tecnologia (Font Awesome); ● Tudo continuar funcionando; ● Projetos grandes sendo desenvolvidos. Adianti Solutions Ltda © Pablo Dall'Oglio Programando para Programadores #13
  • 14.
    Estrutura de diretórios Isolamento entre app (user) e lib (framework, third parts). A carga das classes do FW é com ele mesmo. Onde geralmente ocorrem as mudanças globais em migrações Adianti Solutions Ltda © Pablo Dall'Oglio Programando para Programadores #14
  • 15.
    Aderência a padrões Namespaces Utilização de class_alias() para compatibilidade Adianti Solutions Ltda © Pablo Dall'Oglio Programando para Programadores #15
  • 16.
    Novos componentes ●Release 1.0.1 – TSpinner, TSlider ● Release 1.0.2 – TTreeview ● Release 1.0.3 – TSortList – TSelect ● Release 2.0 Sem Traumas! – TMultiSearch, T...ActionGroup – TExpander, TInputDialog, TColor Adianti Solutions Ltda © Pablo Dall'Oglio Programando para Programadores #16
  • 17.
    Novas features ●Release 1.0.1 – Edição inline (jquery.editinplace.js) – setExitAction (+ método) ● Release 1.0.2 Migration: Acréscimos, substituições em /lib – TEntry::setNumericMask (jquery.maskedinput.js) ● Release 1.0.3 – Nova TFile (TFile.php, tfile.js, tfile.css, same interface); ● Release 2.0 – Datagrid popover (jquery → bootstrap, libraries.html) Adianti Solutions Ltda © Pablo Dall'Oglio Programando para Programadores #17
  • 18.
    Bibliotecas utilizadas <!--Third part libraries required by Adianti Framework --> <script src="lib/jquery/jquery.min.js"></script> <script src="lib/bootstrap/js/bootstrap.min.js"></script> <script src="lib/bootstrap/js/bootstrap-colorpicker.min.js"></script> <script src="lib/jquery/jquery-ui.min.js"></script> <script src="lib/jquery/jquery.blockUI.min.js"></script> <!-- Adianti Framework core and components --> <script src="lib/adianti/include/adianti.min.js"></script> <script src="lib/adianti/include/components.min.js"></script> <!-- Application custom Javascript (Optional) --> <script src="app/lib/include/application.js"></script> <!-- Third part CSS required by Adianti Framework --> <link href="lib/jquery/jquery-ui.min.css"/> <link href="lib/bootstrap/css/bootstrap.min.css"/> <link href="lib/font-awesome/css/font-awesome.min.css"/> <!-- Adianti Framework Components CSS --> <link href="lib/adianti/include/adianti.min.css"/> <link href="lib/adianti/include/components.css"/> <!-- Application custom CSS --> <link href="app/templates/{template}/application.css"/> Bootstrap Dialogs, buttons, dropdown, menubar, tooltip, color picker, date picker, multisearch libraries.html Adianti Solutions Ltda © Pablo Dall'Oglio Programando para Programadores #18
  • 19.
    #Go Horse 2 class ContaReceber { function inserir($id, $a, $b, $c) { $sql = "INSERT INTO conta_receber..."; // exec $sql; } function listar() { $sql = "SELECT * FROM conta_receber"; // exec $sql } function getContasEmAberto() { $sql = "SELECT id, sum(valor) Começou a separar FROM contas_receber cr, lancamentos l WHERE l.conta_id = cr.id GROUP BY 1 HAVING sum(valor) >1”; } function getContasEmAtraso() { $sql = "SELECT ... ... WHERE dt_vencimento <= date(now())"; } Adianti Solutions Ltda © Pablo Dall'Oglio Programando para Programadores #19 } Lógicas complexas extensas em SQL Código reflexo do BD
  • 20.
    #Go Horse SP DROP PROCEDURE IF EXISTS `VALIDAVENDA`; CREATE PROCEDURE `VALIDAVENDA`(IN ICODVENDA int(11), OUT CERRO VARCHAR(60)) BEGIN SET @CCREDITO = (select vendas.cod_venda from vendas where vendas.cod_venda = ICODVENDA and vendas.status = 1); SET @PRODUTO = (select vendas.cod_prod from vendas where vendas.cod_venda = ICODVENDA and vendas.status = 0); SET @COD_TITULO = (select contas_receber.cod_titulo from contas_receber where contas_receber.cod_venda = ICODVENDA); SELECT IF(@CCREDITO IS NULL,'OK','Já existe credito para a venda referente a devolução!') INTO CERRO; IF CERRO = 'OK' THEN UPDATE PRODUTO SET PRODUTO.FLAG_VENDA = "N" WHERE PRODUTO.COD_PROD = @PRODUTO; UPDATE CONTAS_RECEBER SET CONTAS_RECEBER.STATUS = "Cancelado" WHERE CONTAS_RECEBER.COD_VENDA = ICODVENDA; WHILE @COD_TITULO IS NOT NULL DO DELETE FROM CR_CAIXA WHERE CR_CAIXA.COD_TITULO = @COD_TITULO; END WHILE; END IF; Como chamo aquela SP do formulário? END; Adianti Solutions Ltda © Pablo Dall'Oglio Programando para Programadores #20
  • 21.
    Orientação a modelos Aplicação orientada à SQL (BAD) ● Inserir autor em livro INSERT INTO autor_livro (autor_id, livro_id) VALUES ('$autor_id', '$livro_id'); ● Retornar os itens da Nota Fiscal SELECT * FROM nf, itens WHERE nf.id = itens.nf_id AND nf.id = '$nf_id' ● Percorrer dados relacionados SELECT * FROM turma, matricula, aluno WHERE turma.id=matricula.turma_id AND matricula.id_aluno = aluno.id AND turma.id = '$turma_id' Cérebro orientado à relações simples chaves entre tabelas Ver 10 piores SQL Adianti Solutions Ltda © Pablo Dall'Oglio Programando para Programadores #21
  • 22.
    Orientação a modelos Aplicação orientada ao domínio (GOOD) XMI → {PHP, SQL} Utilização de relações mais complexas. Modelo relacional é consequência Adianti Solutions Ltda © Pablo Dall'Oglio Programando para Programadores #22
  • 23.
    Orientação a modelos Exemplo de implementação class Filme extends TRecord { public function get_genero() { if (empty($this->genero)) { Lazy Load $this->genero = new Genero( $this->genero_id ); } return $this->genero; } public function addAtor( Ator $object ) { $this->atores[] = $object; } public function getAtores() { return $this->atores; } public function load($id) { $this->criticas = parent::loadComposite('Critica', ...); $this->atores = parent::loadAggregate('Ator', ...); return parent::load($id); } Adianti Solutions Ltda © Pablo Dall'Oglio Programando para Programadores #23 } Relações mais complexas, é preciso treinar... Métodos auxiliares
  • 24.
    Orientação a modelos Exemplo de utilização // carrega o filme $filme = new Filme(5); print $filme->distribuidor->nome; print $filme->genero->nome; // carrega o filme $filme = new Filme(5); foreach ($filme->getAtores() as $ator) { print $ator->nome; Adianti Solutions Ltda © Pablo Dall'Oglio Programando para Programadores #24 } // adiciona o ator $ator = new Ator(10); $filme->addAtor( $ator ); $filme->store(); LEGIBILIDADE Lazy Load MAIOR Agregação
  • 25.
    Orientação a modelos Aplicação orientada ao domínio (GOOD) ● Inserir autor em livro $livro = new Livro( 10 ); $livro->addAutor( new Autor(8) ); $livro->store(); ● Retornar os itens da Nota Fiscal $nf = new NotaFiscal(10); foreach ($nf->getItems() as $item) { print $item->produto->descricao; } ● Percorrer dados relacionados NAVEGABILIDADE entre relações $turma = new Turma(10); foreach ($turma->getMatriculas() as $matricula) { print $matricula->aluno->nome; } Adianti Solutions Ltda © Pablo Dall'Oglio Programando para Programadores #25
  • 26.
    Persistência Armazenar umnovo objeto (INSERT). //... TTransaction::open('samples'); // abre uma transação $object = new Pessoa; $object->name = 'Maria da Silva'; $object->address = 'Rua da Conceicao'; $object->phone = '(51) 8111-2222'; $object->birthdate = '2013-02-15'; $object->status = 'S'; $object->email = 'maria@email.com'; $object->gender = 'M'; $object->store(); // armazena o objeto TTransaction::close(); // fecha a transação. //... Adianti Solutions Ltda © Pablo Dall'Oglio Programando para Programadores #26
  • 27.
    Persistência Alterar umobjeto já existente (UPDATE). //... TTransaction::open('samples'); // abre uma transação $objeto = new Customer; // instancia o cliente $customer= $objeto->load(31); // carrega o cliente 31 if ($customer) // se existe { $customer->phone = '51 8111-3333'; // muda o fone $customer->store(); // armazena o objeto } new TMessage('info', 'Objeto atualizado'); TTransaction::close(); // fecha a transação. //... Adianti Solutions Ltda © Pablo Dall'Oglio Programando para Programadores #27
  • 28.
    Persistência Manipular umconjunto de objetos conforme um filtro. //... TTransaction::open('samples'); // abre uma transação $repository = new TRepository('Customer'); $customers = $repository->where('gender', '=', 'M') ->where('name', 'like', 'A%') ->load(); foreach ($customers as $customer) { $customer->phone = '84 '.substr($customer->phone, 3); $customer->store(); } Active Record TTransaction::close(); // fecha a transação. //... Repository Pattern Adianti Solutions Ltda © Pablo Dall'Oglio Programando para Programadores #28
  • 29.
    Object caching Melhoriaspara a versão 2.0: ● Cache Service class Customer extends TRecord { 1/3 tempo na matrícula const TABLENAME = 'customer'; const PRIMARYKEY = 'id'; const IDPOLICY = 'max'; // {max, serial} const CACHECONTROL = 'Cache-Control-Class'; // ... } ● Utilizado em: Grandes ganhos de Performance Deve implementar classe específica – Carregar objeto, Salvar objeto, Excluir objeto; – Carregar coleções, Excluir coleções; Adianti Solutions Ltda © Pablo Dall'Oglio Programando para Programadores #29
  • 30.
    Object caching Implementaçãode cache usando APC: class TAPCache implements AdiantiRegistryInterface { public static function enabled() { return extension_loaded('apc'); } public static function setValue($key, $value) { return apc_store($key, serialize($value)); } public static function getValue($key) { return unserialize(apc_fetch($key)); } public static function delValue($key) { return apc_delete($key); } } Adianti Solutions Ltda © Pablo Dall'Oglio Programando para Programadores #30
  • 31.
    Prepared Statements Melhoriaspara a versão 2.0: ● Prepared Statements transparente: – Habilitar/desabilitar por transação (performance); $data = $this->form->getData(); Query Object pattern TTransaction::open('samples'); $criteria = new TCriteria; $criteria->add(new TFilter('gender', '=', $data->gender)); $criteria->add(new TFilter('age', '>', $data->age)); $criteria->add(new TFilter('haircolor', '>', $data->haircolor)); $repository = new TRepository('Customer'); $customers = $repository->load($criteria); TTransaction::close(); Prepared Adianti Solutions Ltda © Pablo Dall'Oglio Programando para Programadores #31
  • 32.
    Prepared Statements Comoo Prepared é processado: TTransaction::log($sql->getInstruction()); $dbinfo = TTransaction::getDatabaseInfo(); if (isset($dbinfo['prep']) AND $dbinfo['prep'] == '1') { Com prepared $result = $conn->prepare($sql->getInstruction(TRUE), ...); $result-> execute( $sql->getPreparedVars() ); } else { Sem prepared $result = $conn-> query($sql->getInstruction()); } if ( $cache = $this->getCacheControl() ) { Atualiza cache $record_key = $class . '['. $this->$pk . ']'; if ($cache::setValue( $record_key, $this->toArray() )) { TTransaction::log($record_key . ' stored in cache'); } } return $result; Adianti Solutions Ltda © Pablo Dall'Oglio Programando para Programadores #32
  • 33.
    Novas restrições ●Release 1.0.2 – TForm::addField() 2 campos mesmo nome; – TForm::addField() só aceita AdiantiWidgetInterface. class TForm { public function addField( AdiantiWidgetInterface $field ) { $name = $field->getName(); if (isset($this->fields[$name])) { throw new Exception(TAdiantiCoreTranslator::translate('You have already added a field called "^1" inside the form', $name)); } $this->fields[$name] = $field; $field->setFormName($this->name); } Adianti Solutions Ltda © Pablo Dall'Oglio Programando para Programadores #33 } Resiliência 1o programador = instrução 2o programador = exceção
  • 34.
    Novas restrições ●Release 1.0.3 – TRecord::addAttribute() não pode ter atributo data; – setLogger() sem transação ativa lança exceção; final class TTransaction { public static function setLogger( TLogger $logger) { if (isset(self::$conn[self::$counter])) { self::$logger[self::$counter] = $logger; } else { Não esperar que as coisas estejam certas throw new Exception(TAdiantiCoreTranslator::translate('No active transactions') . ': ' . __METHOD__); } } Adianti Solutions Ltda © Pablo Dall'Oglio Programando para Programadores #34 } Controle
  • 35.
    Novas restrições ●Release 2.0 (PSR-3) – TTransaction implements LoggerAwareInterface; – TLogger's implements LoggerInterface. final class TTransaction implements LoggerAwareInterface { Ciência public static function setLogger( LoggerInterface $logger) { if (isset(self::$conn[self::$counter])) { self::$logger[self::$counter] = $logger; } else { throw new Exception(TAdiantiCoreTranslator::translate('No active transactions') . ': ' . __METHOD__); } } Adianti Solutions Ltda © Pablo Dall'Oglio Programando para Programadores #35 } Antes, TLogger
  • 36.
    E o quemais? ● Práticas! – Onde colocar tal método? M,V, ou C? Qual das M? – Nunca a Model deve dar TMessage; – Model não deve dar return em falha, só throw; – Nunca abrir transaction na model (Model agnóstica); – Catch sempre na controller; – Transactions somente na controller; – Usar getByAlgumaCoisa(). Ex: Turma::getByCodigo(); – Objetos de negócio sempre em Português (Aluno); – Métodos-ação imprimeAluno(), geraCertificado(). Adianti Solutions Ltda © Pablo Dall'Oglio Programando para Programadores #36
  • 37.
  • 38.
    Studio Pro <?php /** * Customer Active Record * @author <your-name-here> */ class Customer extends TRecord { public function get_city() public function addSkill(Skill $skill) public function getSkills() public function load($id) public function store() public function delete($id = NULL) Adianti Solutions Ltda © Pablo Dall'Oglio Programando para Programadores #38 } ?> XMI SQL PHP Modelo Astah StarUML
  • 39.
    Studio Pro ●Se o modelo for feito antes, facilita tudo; ● Relações podem ser definidas por wizards também. <?php // load customer $obj = new Customer(5); print $obj->city->name; print $obj->category->name; // percorre contatos foreach ($obj->getContacts() as $contact) { $contact->value = '9'.$contact->value; $contact->store(); print $contact->type . '-' . $contact->value; } foreach ($obj->getSkills() as $skill) { print $skill->name; } Adianti Solutions Ltda © Pablo Dall'Oglio Programando para Programadores #39
  • 40.
    Studio Form Designer Adianti Solutions Ltda © Pablo Dall'Oglio Programando para Programadores #40
  • 41.
    Studio Form Designer class TestView extends TPage { private $form; function __construct() { parent::__construct(); $this->form = new TForm; try { Wrapper $ui = new TUIBuilder(500,300); $ui->setController($this); $ui->setForm($this->form); $ui->parseFile('app/forms/sample.form.xml'); $this->form->add($ui); $this->form->setFields($ui->getFields()); } catch (Exception $e) { new TMessage('error', $e->getMessage()); } parent::add($this->form); Adianti Solutions Ltda © Pablo Dall'Oglio Programando para Programadores #41
  • 42.
    Studio PDF Designer Adianti Solutions Ltda © Pablo Dall'Oglio Programando para Programadores #42
  • 43.
    Studio PDF Designer class PDFDesignNFEView extends TPage { function onGenerate() { Wrapper try { $designer = new TPDFDesigner; $designer->fromXml('app/reports/nfe.pdf.xml'); $designer->generate(); $designer->SetFont('Arial', 'B', 8); $designer->setFontColorRGB( '#4C4491' ); $designer->writeAtAnchor('bairro', 'Centro'); $designer->writeAtAnchor('municipio', 'Cidade teste'); $designer->writeAtAnchor('fone', '(11) 1234-5678'); $designer->gotoAnchorXY('details'); $designer->SetFont('Arial', '', 8); $designer->Cell( 62, 10, '12121212', 1, 0, 'C'); $designer->Cell(140, 10, utf8_decode('Guaraná'), 1, 0, 'L'); } //... Adianti Solutions Ltda © Pablo Dall'Oglio Programando para Programadores #43
  • 44.
    Obrigado ● AdiantiFramework: – www.adianti.com.br/framework ● Contato: – www.dalloglio.net – www.adianti.com.br – @pablodalloglio – @adiantisolution ● Não esquecer de falar do Sorteio! Adianti Solutions Ltda © Pablo Dall'Oglio Programando para Programadores #44