quarta-feira, 7 de abril de 2010

Novo endereço

Migrei o meu blog pro wordpress e postarei agora em http://www.abacha.blogsfera.com.br/blog/
[]'s

terça-feira, 30 de março de 2010

CakePHP: Rotas customizáveis

Uma dica bem simples porém muito útil!
Já quis setar uma action como default, acessando assim apenas com controller e o id?
Por exemplo, no meu caso para visualizar produtos de uma categoria, deveria acessar o controller "categorias", a action "view" e passar o nome da categoria (ficando assim: /categorias/view/Water)
Mas para ficar mais "user friendly", gostaria de acessar assim: /categorias/Water.
Para fazer isso, basta alterar o arquivo de rotas (app/config/routes.php) e adicionar algo como:
Router::connect('/categorias/*', array('controller' => 'categorias', 'action' => 'view'));

Vai testar? Use fixtures!

Uma prática necessária na hora de trabalhar com testes é trabalhar com fixtures.
Mas o que são fixtures? Fixtures são dados que são usados para se ter um controle total dos testes.
Com fixtures, não precisamos nos preocupar se um teste altera dados do banco por que a cada teste esses dados são renovados.
A implementação de fixtures no cakephp é muito simples, basta criar o arquivo app/tests/fixtures/nome-do-modelo_fixtures.php com a seguinte estrutura:
class Nome-do-modeloFixture extends CakeTestFixture {
 var $name = 'Nome-do-modelo';
 var $fields = array(
  'id' => array('type'=>'integer', 'null' => false, 'default' => NULL, 'key' => 'primary'),
  'descricao' => array('type'=>'string', 'null' => false, 'default' => NULL)
                //aqui são listados todos os campos da tabela
 );
    
 var $records = array( //aqui vai os dados
                array(
                    'id' => 1,
                    'descricao' => 'teste1'
                ),
                array(
                    'id' => 2,
                    'descricao' => 'teste2'
                )
        );
}
Pelo bake, o usuário pode gerar esse arquivo automaticamente, e se já existir a tabela povoada, pode importar os registros facilmente

Agora, no arquivo do teste basta incluir essa fixture com o prefixo 'app.', como por exemplo:
class Nome-do-modeloTestCase extends CakeTestCase {
 var $fixtures = array ('app.Nome-do-modelo');
}

segunda-feira, 29 de março de 2010

Upload assíncrono de arquivos em CakePHP

Após várias horas tentando implementar um upload de múltiplos arquivos de forma assíncrona no CakePHP, finalmente consegui desenvolver algo exatamente como eu queria, vou explicar a seguir como fiz:
Primeiramente baixei o plugin Uploadify do jQuery e distribui os arquivos da seguinte forma:
- /app/webroot/js/: jquery-1.3.2.min, jquery.uploadify.v2.1.0.min e swfobject
- /app/webroot/css/: uploadify.css
- app/webroot/uploadify/: uploadify.php, uploadify.swf e cancel.png
* criar uma sub-pasta chamada files(app/webroot/uploadify/files) que será usada para guardar temporariamente os arquivos de upload.

Fiz uma alteração no arquivo uploadify.php e descomentei tudo que estava comentado para verificar o tipo do arquivo e criar o diretório se não existir.
O arquivo ficará assim:
if (!empty($_FILES)) {
 $tempFile = $_FILES['Filedata']['tmp_name'];
 $targetPath = $_SERVER['DOCUMENT_ROOT'] . $_REQUEST['folder'] . '/';
 $targetFile =  str_replace('//','/',$targetPath) . $_FILES['Filedata']['name'];
 
 $fileTypes  = str_replace('*.','',$_REQUEST['fileext']);
 $fileTypes  = str_replace(';','|',$fileTypes);
 $typesArray = split('\|',$fileTypes);
 $fileParts  = pathinfo($_FILES['Filedata']['name']);
 
 if (in_array($fileParts['extension'],$typesArray)) {
  // Uncomment the following line if you want to make the directory if it doesn't exist
  mkdir(str_replace('//','/',$targetPath), 0755, true);
  
  move_uploaded_file($tempFile,$targetFile);
  echo "1";
 } else {
 echo 'Invalid file type.';
 }
}

Agora na view, importarei os javascripts e css's:
<?=$javascript->link(array("jquery-1.3.2.min", "jquery.uploadify.v2.1.0.min", "swfobject"))?>
<?=$html->css(array("uploadify"));?>

além de adicionar o código parar criar o botão de upload e o botão para limpar a fila:
<?= $form->file("upload") ?>
<a href="javascript:jQuery('#BookUpload').uploadifyUpload();">Upload</a>
<a href="javascript:jQuery('#BookUpload').uploadifyClearQueue();">Clear</a>
no caso, o id do meu input é "Upload" e como estou no form "Book" devo referenciá-lo como "#BookUpload"

E criarei o seguinte bloco javascript:
<script type="text/javascript">
    jQuery.noConflict();
    jQuery(document).ready(function() {
        jQuery('#BookUpload').uploadify({
            'uploader':'/uploadify/uploadify.swf',
            'script':'/uploadify/uploadify.php',
            'cancelImg':'/uploadify/cancel.png',
            'folder':'/uploadify/files/<?=$_SESSION["Config"]["userAgent"]?>',
            'fileExt':'*.zip;*.pdf;*.rar;*.txt;*.doc;*.xls;*.docx;*.xlsx;*.chm;*.tar;*.tar.gz;*.tgz;*.tar.bz;*.tar.bz2',
            'fileDesc':'*.zip;*.pdf;*.rar;*.txt;*.doc;*.xls;*.docx;*.xlsx;*.chm;*.tar;*.tar.gz;*.tgz;*.tar.bz;*.tar.bz2',
            'sizeLimit':104857600,
            'multi':true
        });
    });
</script>

Irei explicar algumas partes relevantes do código acima:

jQuery.noConflict();: como utilizo nessa mesma view a prototype, não posso utilizar o "$" para chamadas jquery além de ter que utilizar essa função para não gerar conflitos.

jQuery('#BookUpload').uploadify({: é necessário informar o id de um input tipo "file"

'folder':'/uploadify/files/<?=$_SESSION["Config"]["userAgent"]?>': Uma grande dúvida que eu tive era como eu iria relacionar um livro que ainda não foi cadastro com os arquivos subidos.
A solução que eu encontrei foi de criar uma pasta temporária com o nome de uma variável de sessão desse usuário, ou seja, enquanto o usuário estiver "em tempo de criação" de um livro, os arquivos que já foram subidos irão ficar nessa pasta temporária. Como apontado pelo Arlindo, no caso de estar rodando em uma máquina windows, deve-se utilizar o endereço completo da pasta. ('folder':'/app/webroot/uploadify/files/...)

'fileExt':'*.zip;*.pdf;*.rar;*.txt;*.doc;*.xls;*.docx;*.xlsx;*.chm;*.tar;*.tar.gz;*.tgz;*.tar.bz;*.tar.bz2': Extensões aceitas pelo upload


'sizeLimit':104857600: Tamanho máximo dos arquivos em bytes (104857600b = 100mb)

'multi':true: Possibilidade de enviar mais de um arquivo


Para finalizar, criei uma função que eu chamo no beforeSave:
function beforeSave() {
        if (isset($_SESSION["Config"]["userAgent"]))
            $this->data["BookFiles"] = $this->uploadFiles();
        return true;
    }
function uploadFiles() {
        $dir = "uploadify/files/".$_SESSION["Config"]["userAgent"]."/";
        if (is_dir($dir)) {
            $handle = opendir($dir);

            while (false !== ($file = readdir($handle)))
                if ($file != "." && $file != "..") {
                    $old_name = $file;
                    $cont = 2;
                    while (file_exists("files/".$file)) {
                        $file = pathinfo($file);
                        $file = $file["filename"]."-".$cont.".".$file["extension"];
                        $cont++;
                    }
                    rename($dir.$old_name, "files/".$file);
                    $files[] = array("name" => $file);
                    chmod("files/".$file, 0755);
                }
            closedir($handle);
            rmdir($dir);
            return $files;
        }
        else return false;
    }

Essa função só será utilizada se o diretório identificado pela variável de sessão existir ("uploadify/files/".$_SESSION["Config"]["userAgent"]."/"), se existir, movo os arquivos para a pasta /app/webroot/files/ (nisso eu verifico se já existe algum arquivo com o mesmo nome, caso existir, renomeio colocando um numeral no fim).
A seguir dou permissão no arquivo (chmod("files/".$file, 0755)) e retorno um array com o nome de todos os arquivos.




Em um próximo momento, irei procurar uma solução para deletar os arquivos que ficarem no servidor quando a sessão for destruída, no caso de uma pessoa subir arquivos mas não submeter o formulário

sexta-feira, 19 de março de 2010

Erro no teste de validação "not null" em campo string no CakePHP

No início das minhas experiências utilizando BDD com o cakephp e a sua biblioteca de testes (CakeTest estendendo SimpleTest), tive um problema que considero no mínimo bizarro.
Vou tentar demonstrar o problema que eu tive e como eu resolvi:
Pra começar irei criar uma tabela simples com apenas um campo id (int) e um campo nome (string):
CREATE TABLE  `teste`.`usuarios` (
`id` INT NOT NULL AUTO_INCREMENT ,
`nome` VARCHAR( 255 ) NOT NULL ,
PRIMARY KEY ( `id` )
)


Agora irei gerar pelo bake o modelo dessa tabela com a validação "notEmpty" (recomendada pelo bake para um campo de string) e também os arquivos de teste:

Possible Models based on your current database:
1. Usuario
Enter a number from the list above, type in the name of another model, or 'q' to exit
[q] > 1
Would you like to supply validation criteria for the fields in your model? (y/n)
[y] > y

[...]

Field: nome
Type: string
---------------------------------------------------------------
Please select one of the following validation options:
---------------------------------------------------------------
[...]
20 - notEmpty
29 - Do not do any validation on this field.
... or enter in a valid regex validation string.

[20] >
Would you like to define model associations (hasMany, hasOne, belongsTo, etc.)? (y/n)
[y] > n

---------------------------------------------------------------
The following Model will be created:
---------------------------------------------------------------
Name: Usuario
Validation: Array
(
[nome] => notempty
)


Baking model class for Usuario...

Creating file /home/duke/www/cakephp/app/models/usuario.php
Wrote /home/duke/www/cakephp/app/models/usuario.php

[...]
Baking test fixture for Usuario...
Creating file /home/duke/www/cakephp/app/tests/fixtures/usuario_fixture.php
Wrote /home/duke/www/cakephp/app/tests/fixtures/usuario_fixture.php

Baking unit test for Usuario...

Creating file /home/duke/www/cakephp/app/tests/cases/models/usuario.test.php
Wrote /home/duke/www/cakephp/app/tests/cases/models/usuario.test.php



Agora abrirei o arquivo de testes do modelo (usuario.test.php) e criarei o seguinte método de teste:

function testAttributes() {
$this->assertFalse($this->Usuario->validates(), 'should be invalid');
$this->Usuario->set("nome", "adriano");
$this->assertTrue($this->Usuario->save(), 'should be valid');
}

Estou fazendo 2 asserts, ou seja, estou garantindo duas coisas:
1. que ao tentar validar um objeto "Usuario" sem ter atribuído nenhum parâmetro, ele será inválido pois o campo "nome" é obrigatório;
2. e que após definir o campo "nome" para "adriano" e tentar salvar, deve ser válido.

Porém ao rodar os testes terei o seguinte resultado:
FAILED
should be invalid at [.../app/tests/cases/models/usuario.test.php line 14]
PASSED [...]
should be valid at [.../app/tests/cases/models/usuario.test.php line 16]

Ou seja, ele falhou no primeiro teste, logo, ele trata o objeto usuário "cru" como um objeto válido.
O meu teste está correto, eu quero que aconteça exatamente como designei nas minhas assertions, porém parece que o cake não entende "notEmpty" da mesma forma que eu.
Para esse teste passar, teria que setar o nome do usuário vazio antes da assertion:
$this->Usuario->set("nome", "");

Ai sim ele falharia o validate e o teste passaria, porém eu quero considerar que se a posição do array de dados do objeto ($this->Usuario->data["nome"]) não existir, é porque ela está vazia.
Para obter esse resultado, eu altero a regra de validação no modelo:
O bake deve gera a validação apenas criando a regra de notempty para o campo:

var $validate = array(
'nome' => array('notempty')
);

e eu alterarei para o campo aceitar todos os caracteres visíveis e ser obrigatório:

var $validate = array(
'rule' => array('custom', '/^[\x20-\x7E]+$/i'),
'required' => true
);

agora se rodar os testes novamente, eles irão passar, mesmo sem declarar como vazio o nome do usuário

sexta-feira, 20 de novembro de 2009

Manifesto anti-google

Eu to de saco cheio do google... tudo que lançam hoje em dia é do google ou parece que é (o twitter tem tudo pra ser). É sempre o mesmo design branco e azul com alguns toques de rosa/vermelho...
JÁ ENXEU O SACO! Não tem mais nada sendo lançado hoje em dia com aquele visual carregado, cheio de cores, que depois de 5 versões eles finalmente conseguem entender que o design tem que mudar.
O google já estabeleceu todos os padrões de uso da internet e é por isso que a internet está ficando monotona.
Eu vejo muito cliente de informática falando: "Não da pra gente colocar uma busca no site tipo a do google?", "Não da pra gente ter uma agenda tipo a do google?", "Não da pra gente ter um sistema de passar vídeos tipo o do google?"
VAI SE FUDER, CONTRATA A PORRA DO GOOGLE PRA FAZER TEU SISTEMA!
A internet era muito mais divertida quando eramos sobrecarregados por cores e sons na época de icq/irc

sábado, 28 de março de 2009

Istambul!

from Adriano "duke" Bacha
to engcomp-europa@googlegroups.com
date Sat, Mar 28, 2009 at 10:00 PM
subject Re: [engcomp-europa:447] RES: [engcomp-europa:445] Istambul!
mailed-by gmail.com
Images from this sender are always displayed. Don't display from now on.

hide details 10:00 PM (35 minutes ago)


Reply


Ah! Esqueci de contar uma coisa de ontem:
A noite, depois que devolvemos os quadriciclos, ficamos na frente do albergue matando tempo porque o nosso barco era so 00:40, dai apareceram 6 americanas que moram em Florenca que tambem iam nesse barco e nos chamaram para ir num bar do lado do albergue.
Fomos la e ficamos conversando, ate que depois de um tempo as duas garconetes do lugar comecaram a dancar uma musica grega e e claro que eu fui atras e ai todo mundo comecou a dancar.
Uma danca estilo de roda, parecia ate quadrilha, la pelas tantas da danca, o dono do lugar comecou a jogar guardanapo pra cima, fazer maior sujeira.. ate ai tudo bem.. mas ai ele comecou a jogar pratos! Achavamos que isso so acontecia em filmes, que era historia, mas ele deve ter quebrado pelo menos uns 5 pratos na danca, foi muito engracado, mesmo que os pratos eram atirados pra cima e quase acertavam a gente! hahahaha