view Lib/IMPL/Web/TT/Document.pm @ 134:44977efed303

Significant performance optimizations Fixed recursion problems due converting objects to JSON Added cache support for the templates Added discovery feature for the web methods
author wizard
date Mon, 21 Jun 2010 02:39:53 +0400
parents 0dce0470a3d8
children 5b849974bed8
line wrap: on
line source

package IMPL::Web::TT::Document;
use strict;
use warnings;

use base qw(IMPL::DOM::Document IMPL::Object::Disposable);
use Template::Context;
use Template::Provider;
use IMPL::Class::Property;
use File::Spec;
use Scalar::Util qw(blessed);
use IMPL::Web::TT::Collection;
use IMPL::Web::TT::Control;
use Carp;

BEGIN {
    private property _provider => prop_all;
    private property _context => prop_all;
    public property cache => prop_all;
    public property template => prop_get | owner_set;
    public property presenter => prop_all, { validate => \&_validatePresenter };
    private property _controlClassMap => prop_all;
}

our %CTOR = (
    'IMPL::DOM::Document' => sub { nodeName => 'document' }
);

sub CTOR {
	my ($this,%args) = @_;
	
	$this->_controlClassMap({});
	$this->registerControlClass( Control => 'IMPL::Web::TT::Control' );
	$this->appendChild( $this->Create(body => 'IMPL::Web::TT::Collection') );
	$this->appendChild( $this->Create(head => 'IMPL::Web::TT::Collection') );
	$this->cache($args{cache}) if $args{cache};
}

sub CreateControl {
	my ($this,$name,$class,$args) = @_;
	
	$args = {} unless ref $args eq 'HASH';
	
	if (my $info = $this->_controlClassMap->{$class}) {
		my %nodeArgs = (%{$info->{args}},%$args);
		$nodeArgs{controlClass} = $class;
		
		return $this->Create($name,$info->{type},\%nodeArgs);
	} else {
		die new IMPL::Exception('A control is\'t registered', $class, $name);
	}
}

sub provider {
    my ($this,%args) = @_;
    
    if (my $provider = $this->_provider) {
        return $provider;
    } else {
        return $this->_provider(new Template::Provider(
            \%args
        ));
    }
}

sub context {
    my ($this) = @_;
    
    if (my $ctx = $this->_context) {
        return $ctx;
    } else {
        return $this->_context (
            new Template::Context(
                VARIABLES => {
                    document => $this,
                    this => $this,
                    render => sub {
                    	$this->_process(@_);
                    }
                },
                TRIM => 1,
                RECURSION => 1,
                LOAD_TEMPLATES => [$this->provider]
            )
        )
    }
}

sub resolveVar {
	my ($this,$var) = @_;
	
	return $this->context->stash->get($var);
}

sub registerControlClass {
	my ($this, $controlClass, $type, $args) = @_;
	
	$type ||= 'IMPL::Web::TT::Control';
	
	die new IMPL::InvalidArgumentException("A controlClass must be a single word",$controlClass) unless $controlClass =~ /^\w+$/;
	
	eval "require $type; 1;" or die new IMPL::Exception("Failed to load a module",$type,"$@") unless eval { $type->can('new') };
	
	die new IMPL::InvalidArgumentException("A type must be subclass of IMPL::DOM::Node",$type) unless $type->isa('IMPL::DOM::Node');
	
	$this->_controlClassMap->{$controlClass} = {
		controlClass => $controlClass,
		type => $type,
		args => ref $args eq 'HASH' ? $args : {}
	};
}

sub require {
	my ($this,$template) = @_;
	
	my $doc = $this->context->template($template);
	
	die new IMPL::InvalidOperationException("A specified template isn't a document",$template) unless eval{ $doc -> isa('Template::Document') };
	
	my $controlClass = $doc->class;
	my $type = $doc->nativeType;
	my $controlTemplate;
	my $out = "";
	
	die new IMPL::InvalidOperationException("A specified template isn't a control",$template) unless $controlClass;
	
	if (not $this->isControlClass($controlClass)) {
		if ($doc->template) {
			$controlTemplate = $doc->blocks()->{$doc->template} || $this->context->template($doc->template);
			$out = $this->context->process($doc);
		} else {
			$controlTemplate = $doc;
		}
		$this->registerControlClass($controlClass,$type,{ template => $controlTemplate } );
	}
	
	return $out;
}

sub isControlClass {
	my ($this,$name) = @_;
	return $this->_controlClassMap->{$name} ? 1 : 0;
}

sub _getControls {
	my ($this) = @_;
	
	my ($node) = $this->selectNodes('controls');
	return $node;
}

sub _validatePresenter {
	my ($this,$value) = @_;
	
	die new IMPL::InvalidArgumentException("A view object is required") unless blessed($value) and $value->isa('Template::View');
}

sub LoadFile {
    my ($this,$filePath,$encoding,@includes) = @_;
    
    die new IMPL::InvalidArgumentException("A filePath parameter is required") unless $filePath;
    
    $encoding ||= 'utf8';
    
    $this->_context(undef);
    $this->_provider(undef);
    
    my ($vol,$dir,$fileName) = File::Spec->splitpath($filePath);
    
    my $inc = File::Spec->catpath($vol,$dir,'');
    
    $this->provider(
        ENCODING => $encoding,
        INTERPOLATE => 1,
        PRE_CHOMP => 1,
        POST_CHOMP => 1,
        COMPILE_EXT => $this->cache ? '.ttc' : undef,
        COMPILE_DIR => $this->cache,
        INCLUDE_PATH => [$inc,@includes]
    );
    
    $this->template($this->context->template($fileName));
}

sub AddVar {
	my ($this,$name,$value) = @_;
	
	$this->context->stash->set($name,$value);
}

sub title {
    $_[0]->template->title;
}

sub Render {
    my ($this) = @_;
    
    return $this->template->process($this->context);
}

# Формирует представление для произвольных объектов 
sub _process {
	my ($this,@items) = @_;
	
	my @result;
	
	foreach my $item (@items) {
		if (blessed($item) and $item->isa('IMPL::Web::TT::Control')) {
			push @result, $item->Render();	
		} elsif(blessed($item)) {
			if ($this->presenter) {
				push @result, $this->presenter->print($item);
			} else {
				push @result, $this->toString;
			}
		} else {
			push @result, $item;
		}
	}
	
	return join '',@result;
}

our $AUTOLOAD;
sub AUTOLOAD {
	my $this = shift;
	my ($method) = ($AUTOLOAD =~ /(\w+)$/);
	
	if($method =~ /^create(\w+)/) {
		my ($name,$args) = @_;
		return $this->CreateControl($name,$1,$args);
	}
	
	my @result = $this->selectNodes($method);
	
	return $result[0] if @result;
	carp "Looks like you have a mistake, document doesn't have a such property or child: $method";
	return;
}

sub as_list {
	$_[0]->childNodes;
}

sub Dispose {
    my ($this) = @_;
    
    $this->template(undef);
    $this->_context(undef);
    $this->_provider(undef);
    
    $this->supercall::Dispose();
}

1;
__END__
=pod

=head1 NAME

C<IMPL::Web::TT::Document> - Документ, позволяющий строить представление по шаблону

=head1 SYNOPSIS

=begin code 

// create new document
my $doc = new IMPL::Web::TT::Document;

// load template
$doc->loadFile('Templates/index.tt');

// render file
print $doc->Render();

=end code

=head1 DESCRIPTION

C<use base qw(IMPL::DOM::Document)>

Документ, основанный на шаблоне Template::Toolkit. Позволяет загрузить шаблон,
и сформировать окончательный документ. Является наследником C<IMPL::DOM::Node>,
т.о. может быть использован для реализации DOM модели.

Внутри шаблона переменная C<document> ссылается на объект документа. По этой
причине образуется циклическая ссылка между объектами шаблона и документом, что
требует вызова метода C<Dispose> для освобождения документа.

=head1 METHODS

=over

=item C<CTOR()>

Создает новый экземпляр документа, свойство C<nodeName> устанавливается в 'C<document>'

=item C<$doc->LoadFile($fileName,$encoding)>

Загружает шаблон из файла C<$fileName>, используя кодировку C<$encoding>. Если
кодировка не указана, использует utf-8.

=item C<$doc->Render()>

Возвращает данные построенные на основе загруженного шаблона.

=item C<$doc->Dispose()>

Освобождает ресурсы и помечает объект как освобожденный.

=back

=head1 DOM

Документ представляет собой DOM документ, состоящий из узлов, которые представляют собой данные
для отображения. Для форматированого вывода используется C<template>.

В качестве элементов документа могут присутсвовать специальные объекты C<IMPL::Web::TT::Control>,
которые внутри содержат шаблон для форматирования собственного содержимого.



Документ предоставляет ряд фнукций для работы с элементами управления.

=head1 TEMPLATE

=begin code html

[% CALL document.registerClass( 'Table', 'My::TableClass', template => 'tables/pretty.tt' ) %]
[% CALL document.registerClass( 'Form' )%]

[% table = document.сreateTable('env') %]

[% FOEACH item in document.result %]
	[% table.rows.Add( item.get('name','value') ) %]
[% END %]

[% form = document.createForm('login') %]
[% form.template = 'LOGIN_FORM'%]

[% FOREACH item IN document.childNodes %]
	[%render(item)%]
[% END %]
	
[% BLOCK LOGIN_FORM %]
<form method="POST" action='/login.pl'>
	user: [% render(this.item('name')) %] password: [% render(this.item('password')) %] <input type="submit"/>
</form>
[% END %]

=end code html

=cut