replaced registry by dedicated static class

rearanged view and controller factories in dispatcher
optimized views
This commit is contained in:
Steffen Vogel 2010-07-20 19:10:45 +02:00
parent d0fe72e41e
commit 096a2daaad
16 changed files with 352 additions and 231 deletions

View file

@ -15,11 +15,13 @@ foreach ($classLoaders as $loader) {
$loader->register(); // register on SPL autoload stack
}
// load configuration into registry
// load configuration
if (!file_exists(BACKEND_DIR . '/volkszaehler.conf.php')) {
throw new Exception('No configuration available! Use volkszaehler.conf.default.php as an template');
}
include BACKEND_DIR . '/volkszaehler.conf.php';
else {
Util\Configuration::load(BACKEND_DIR . '/volkszaehler.conf.php');
}
$em = Volkszaehler\Dispatcher::createEntityManager();

View file

@ -44,12 +44,7 @@ foreach ($classLoaders as $loader) {
// enable strict error reporting
error_reporting(E_ALL);
// load configuration into registry
if (!file_exists(BACKEND_DIR . '/volkszaehler.conf.php')) {
throw new Exception('No configuration available! Use volkszaehler.conf.default.php as an template');
}
include BACKEND_DIR . '/volkszaehler.conf.php';
Util\Configuration::load(BACKEND_DIR . '/volkszaehler.conf');
$fc = new Dispatcher; // spawn frontcontroller / dispatcher
$fc->run(); // execute controller and sends output

View file

@ -33,19 +33,6 @@ abstract class Controller {
$this->em = $em;
}
/*
* creates new view instance depending on the requested format
*/
public static function factory(\Volkszaehler\View\View $view, \Doctrine\ORM\EntityManager $em) {
$controller = ucfirst(strtolower($view->request->getParameter('controller')));
$controllerClassName = 'Volkszaehler\Controller\\' . $controller;
if (!(\Volkszaehler\Util\ClassLoader::classExists($controllerClassName)) || !is_subclass_of($controllerClassName, '\Volkszaehler\Controller\Controller')) {
throw new \InvalidArgumentException('\'' . $controllerClassName . '\' is not a valid controller');
}
return new $controllerClassName($view, $em);
}
/**
* run controller actions
*

View file

@ -30,30 +30,54 @@ use Volkszaehler\Util;
*/
final class Dispatcher {
// MVC
private $em = NULL; // Model (Doctrine EntityManager)
private $view = NULL; // View
private $controller = NULL; // Controller
protected $em; // Model (Doctrine EntityManager)
protected $view; // View
protected $controller; // Controller
/*
* constructor
*/
public function __construct() {
// create HTTP request & response (needed to initialize view & controller)
$request = new View\Http\Request();
$response = new View\Http\Response();
$format = $request->getParameter('format');
$format = ($request->getParameter('format')) ? $request->getParameter('format') : 'json'; // default action
$controller = $request->getParameter('controller');
// initialize entity manager
$this->em = Dispatcher::createEntityManager();
$this->view = View\View::factory($request, $response);
$this->controller = Controller\Controller::factory($this->view, $this->em);
// initialize view
if (in_array($format, array('png', 'jpeg', 'gif'))) {
$this->view = new View\JpGraph($request, $response, $format);
}
else {
if ($controller == 'data' && ($format == 'json' || $format == 'xml')) {
$controller = 'channel';
}
$viewClassName = 'Volkszaehler\View\\' . ucfirst($format) . '\\' . ucfirst($controller);
if (!(\Volkszaehler\Util\ClassLoader::classExists($viewClassName)) || !is_subclass_of($viewClassName, '\Volkszaehler\View\View')) {
throw new \InvalidArgumentException('\'' . $viewClassName . '\' is not a valid View');
}
$this->view = new $viewClassName($request, $response);
}
// initialize controller
$controllerClassName = 'Volkszaehler\Controller\\' . ucfirst(strtolower($request->getParameter('controller')));
if (!(\Volkszaehler\Util\ClassLoader::classExists($controllerClassName)) || !is_subclass_of($controllerClassName, '\Volkszaehler\Controller\Controller')) {
throw new \InvalidArgumentException('\'' . $controllerClassName . '\' is not a valid controller');
}
$this->controller = new $controllerClassName($this->view, $this->em);
}
/**
* execute application
*/
public function run() {
$action = (is_null($this->view->request->getParameter('action'))) ? 'get' : $this->view->request->getParameter('action'); // default action
$action = ($this->view->request->getParameter('action')) ? 'get' : $this->view->request->getParameter('action'); // default action
$this->controller->run($action); // run controllers actions (usually CRUD: http://de.wikipedia.org/wiki/CRUD)
$this->view->render(); // render view & send http response
@ -62,30 +86,27 @@ final class Dispatcher {
/**
* factory for doctrines entitymanager
*
* @todo create extra singleton class or registry?
* @todo create extra singleton class?
*/
public static function createEntityManager() {
$vzConfig = Util\Registry::get('config');
// Doctrine
$dcConfig = new \Doctrine\ORM\Configuration;
$config = new \Doctrine\ORM\Configuration;
if (extension_loaded('apc')) {
$cache = new \Doctrine\Common\Cache\ApcCache;
$dcConfig->setMetadataCacheImpl($cache);
$dcConfig->setQueryCacheImpl($cache);
$config->setMetadataCacheImpl($cache);
$config->setQueryCacheImpl($cache);
}
$driverImpl = $dcConfig->newDefaultAnnotationDriver(BACKEND_DIR . '/lib/Model');
$dcConfig->setMetadataDriverImpl($driverImpl);
$driverImpl = $config->newDefaultAnnotationDriver(BACKEND_DIR . '/lib/Model');
$config->setMetadataDriverImpl($driverImpl);
$dcConfig->setProxyDir(BACKEND_DIR . '/lib/Model/Proxies');
$dcConfig->setProxyNamespace('Volkszaehler\Model\Proxies');
$dcConfig->setAutoGenerateProxyClasses(DEV_ENV == true);
$config->setProxyDir(BACKEND_DIR . '/lib/Model/Proxies');
$config->setProxyNamespace('Volkszaehler\Model\Proxies');
$config->setAutoGenerateProxyClasses(DEV_ENV == true);
$dcConfig->setSQLLogger(Util\Debug::getSQLLogger());
$config->setSQLLogger(Util\Debug::getSQLLogger());
$em = \Doctrine\ORM\EntityManager::create($vzConfig['db'], $dcConfig);
$em = \Doctrine\ORM\EntityManager::create(Util\Configuration::read('db'), $config);
return $em;
}

View file

@ -66,6 +66,10 @@ abstract class Interpreter implements InterpreterInterface {
case 'minute':
$sqlGroupBy = 'YEAR(' . $ts . '), DAYOFYEAR(' . $ts . '), HOUR(' . $ts . '), MINUTE(' . $ts . ')';
break;
case 'second':
$sqlGroupBy = 'YEAR(' . $ts . '), DAYOFYEAR(' . $ts . '), HOUR(' . $ts . '), MINUTE(' . $ts . '), SECOND(' . $ts . ')';
break;
default:
if (is_numeric($groupBy)) { // lets agrregate it with php

View file

@ -0,0 +1,112 @@
<?php
/*
* Copyright (c) 2010 by Justin Otherguy <justin@justinotherguy.org>
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License (either version 2 or
* version 3) as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*
* For more information on the GPL, please go to:
* http://www.gnu.org/copyleft/gpl.html
*/
namespace Volkszaehler\Util;
class Configuration {
static public $values = array(); // TODO protected
static public function write($var, $value) {
if (!is_scalar($value) && !is_array($value)) {
throw new \Exception('sry we can\'t store this datatype in the configuration');
}
$values =& self::$values;
$tree = explode('.', $var);
foreach ($tree as $part) {
$values =& $values[$part];
}
$values = $value;
}
static public function read($var = NULL) {
$tree = explode('.', $var);
if (is_null($var)) {
return self::$values;
}
$values = self::$values;
foreach ($tree as $part) {
$values = $values[$part];
}
return $values;
}
static public function delete($var) {
}
/*
* configuration file handling
*/
static public function load($filename) {
$filename .= '.php';
if (!file_exists($filename)) {
throw new \Exception('configuration file not found: ' . $filename);
}
include $filename;
if (!isset($config)) {
throw new \Exception('no variable $config found in ' . $filename);
}
self::$values = $config;
}
static public function store($filename) {
$filename .= '.php';
$delcaration = '';
foreach (self::$values as $key => $value) {
$export = var_export($value, true);
$export = preg_replace('/=>\s+array/', '=> array', $export);
$export = str_replace(" ", "\t", $export);
$declaration .= '$config[\'' . $key . '\'] = ' . $export . ';' . PHP_EOL . PHP_EOL;
}
$content = <<<EOT
<?php
/*
* That's the volkszaehler.org configuration file.
* Please take care of the following rules:
* - you are allowed to edit it by your own
* - anything else than the \$config declaration
* will maybe be removed during the reconfiguration
* by the configuration parser!
* - only literals are allowed as parameters
* - expressions will be evaluated by the parser
* and saved as literals
*/
$declaration?>
EOT;
return file_put_contents($filename, $content);
}
}
?>

View file

@ -1,103 +0,0 @@
<?php
/*
* Copyright (c) 2010 by Justin Otherguy <justin@justinotherguy.org>
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License (either version 2 or
* version 3) as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*
* For more information on the GPL, please go to:
* http://www.gnu.org/copyleft/gpl.html
*/
namespace Volkszaehler\Util;
/**
* Registry class to pass global variables between classes.
*/
abstract class Registry {
/**
* Object registry provides storage for shared objects
*
* @var array
*/
protected static $registry = array();
/**
* Adds a new variable to the Registry.
*
* @param string $key Name of the variable
* @param mixed $value Value of the variable
* @throws Exception
* @return bool
*/
public static function set($key, $value) {
if (!isset(self::$registry[$key])) {
self::$registry[$key] = $value;
return true;
} else {
throw new \Exception('Unable to set variable `' . $key . '`. It was already set.');
}
}
/**
* Returns the value of the specified $key in the Registry.
*
* @param string $key Name of the variable
* @return mixed Value of the specified $key
*/
public static function get($key)
{
if (isset(self::$registry[$key])) {
return self::$registry[$key];
}
return null;
}
/**
* Returns the whole Registry as an array.
*
* @return array Whole Registry
*/
public static function getAll()
{
return self::$registry;
}
/**
* Removes a variable from the Registry.
*
* @param string $key Name of the variable
* @return bool
*/
public static function remove($key)
{
if (isset(self::$registry[$key])) {
unset(self::$registry[$key]);
return true;
}
return false;
}
/**
* Removes all variables from the Registry.
*
* @return void
*/
public static function removeAll()
{
self::$registry = array();
return;
}
}
?>

View file

@ -21,6 +21,8 @@
namespace Volkszaehler\View\Csv;
use Volkszaehler\Util;
abstract class Csv extends \Volkszaehler\View\View {
protected $csv = array();
protected $header = array();
@ -35,8 +37,8 @@ abstract class Csv extends \Volkszaehler\View\View {
$this->header[] = 'source: volkszaehler.org';
$this->header[] = 'version: ' . \Volkszaehler\VERSION;
$this->response->setHeader('Content-type', 'text/csv');
$this->response->setHeader('Content-Disposition', 'attachment; filename="data.csv"');
$this->response->setHeader('Content-type', 'text/plain');
//$this->response->setHeader('Content-Disposition', 'attachment; filename="data.csv"');
}
public function render() {
@ -60,13 +62,13 @@ abstract class Csv extends \Volkszaehler\View\View {
}
public function addDebug() {
$config = \Volkszaehler\Util\Registry::get('config');
$this->footer[] = 'time: ' . $this->getTime();
$this->footer[] = 'database: ' . $config['db']['driver'];
$this->footer[] = 'database: ' . Util\Configuration::read('db.driver');
foreach (\Volkszaehler\Util\Debug::getSQLLogger()->queries as $query) {
$this->footer[] = 'query: ' . $query['sql'];
$this->footer[] = ' parameters: ' . implode(', ', $query['parameters']);
}
}

View file

@ -32,34 +32,44 @@ require_once \Volkszaehler\BACKEND_DIR . '/lib/vendor/JpGraph/jpgraph_date.php';
* @todo unifiy axes of same unit
*/
class JpGraph extends View {
protected $width = 800;
protected $height = 600;
/*
* indicator => ynaxis[n] mapping
*/
protected $axes = array();
protected $channels = array();
protected $width = 800;
protected $height = 400;
protected static $colors = array('chartreuse', 'chocolate1', 'cyan', 'blue', 'lightcyan4', 'gold');
protected $graph;
/*
* constructor
*/
public function __construct(Http\Request $request, Http\Response $response, $format) {
parent::__construct($request, $response);
$this->graph = new \Graph($this->width,$this->height);
$this->graph->img->SetImgFormat($format);
// Specify what scale we want to use,
$this->graph->SetScale('datlin');
$this->graph->legend->setPos(0.15,0.025, 'left', 'top');
$this->graph->legend->SetPos(0.1,0.02, 'left', 'top');
$this->graph->legend->SetShadow(false);
$this->graph->SetMarginColor('white');
$this->graph->SetMargin(90,65,10,90);
$this->graph->SetYDeltaDist(65);
$this->graph->yaxis->SetTitlemargin(36);
$this->graph->SetTickDensity(TICKD_DENSE, TICKD_SPARSE);
$this->graph->xaxis->SetFont(FF_ARIAL);
$this->graph->xaxis->SetLabelAngle(60);
$this->graph->xaxis->SetLabelAngle(45);
$this->graph->xaxis->SetLabelFormatCallback(function($label) { return date('j.n.y G:i', $label); });
//$this->graph->img->SetAntiAliasing();
@ -73,51 +83,63 @@ class JpGraph extends View {
$yData[] = $reading['value'];
}
// Create the linear plot
// Create the scatter plot
$plot = new \ScatterPlot($yData, $xData);
$plot->setLegend($obj->getName() . ': ' . $obj->getDescription() . ' [' . $obj->getUuid() . ']');
$plot->setLegend($obj->getName() . ': ' . $obj->getDescription() . ' [' . $obj->getUnit() . ']');
$plot->SetLinkPoints(true, self::$colors[$count]);
$plot->mark->SetColor(self::$colors[$count]);
$plot->mark->SetFillColor(self::$colors[$count]);
$plot->mark->SetType(MARK_DIAMOND);
$plot->mark->SetWidth(1);
$plot->SetLinkPoints(true, self::$colors[$count]);
if ($count == 0) {
$yaxis = $this->graph->yaxis;
$this->graph->Add($plot);
$axis = $this->getAxisIndex($obj);
if ($axis >= 0) {
$this->graph->AddY($axis, $plot);
}
else {
$this->graph->SetYScale($count-1,'lin');
$yaxis = $this->graph->ynaxis[$count-1];
$this->graph->SetMargin(60,($count) * 65,10,90);
$this->graph->AddY($count-1, $plot);
$this->graph->Add($plot);
}
$yaxis->title->Set($obj->getUnit());
$yaxis->title->SetFont(FF_ARIAL);
$yaxis->SetColor(self::$colors[$count]);
$yaxis->SetTitleMargin('50');
$this->channels[] = $obj;
}
public function addException(\Exception $e) { echo $e; }
public function addDebug() {}
public static function factory(Http\Request $request, Http\Response $response) {
protected function getAxisIndex(\Volkszaehler\Model\Channel $obj) {
if (!in_array($obj->getIndicator(), array_keys($this->axes))) {
$count =count($this->axes);
if ($count == 0) {
$this->axes[$obj->getIndicator()] = -1;
$yaxis = $this->graph->yaxis;
}
else {
$this->axes[$obj->getIndicator()] = $count - 1;
$this->graph->SetYScale($this->axes[$obj->getIndicator()],'lin');
$yaxis = $this->graph->ynaxis[$this->axes[$obj->getIndicator()]];
}
$yaxis->title->Set($obj->getUnit());
$yaxis->SetFont(FF_ARIAL);
$yaxis->title->SetFont(FF_ARIAL);
$yaxis->SetTitleMargin('50');
}
return $this->axes[$obj->getIndicator()];
}
public function render() {
$this->graph->SetMargin(75, (count($this->axes) - 1) * 65 + 10, 20, 90);
// Display the graph
$this->graph->Stroke();
parent::render();
}
}
?>

View file

@ -39,13 +39,22 @@ abstract class Json extends \Volkszaehler\View\View {
}
public function render() {
echo self::format(json_encode($this->json));
parent::render();
// TODO solve rendering order problem
$json = json_encode($this->json);
if ($this->request->getParameter('debug')) {
echo self::format($json);
}
else {
echo $json;
}
}
protected static function format($json) {
$tab = "\t";
$formatted = '';
$indentLevel = 0;
$inString = false;
@ -59,21 +68,21 @@ abstract class Json extends \Volkszaehler\View\View {
$formatted .= $char;
if (!$inString && (ord($json[$c+1]) != ord($char)+2)) {
$indentLevel++;
$formatted .= "\n" . str_repeat($tab, $indentLevel);
$formatted .= "\n" . str_repeat("\t", $indentLevel);
}
break;
case '}':
case ']':
if (!$inString && (ord($json[$c-1]) != ord($char)-2)) {
$indentLevel--;
$formatted .= "\n" . str_repeat($tab, $indentLevel);
$formatted .= "\n" . str_repeat("\t", $indentLevel);
}
$formatted .= $char;
break;
case ',':
$formatted .= $char;
if (!$inString) {
$formatted .= "\n" . str_repeat($tab, $indentLevel);
$formatted .= "\n" . str_repeat("\t", $indentLevel);
}
break;
case ':':
@ -96,10 +105,8 @@ abstract class Json extends \Volkszaehler\View\View {
}
public function addDebug() {
$config = Util\Registry::get('config');
$this->json['debug'] = array('time' => $this->getTime(),
'database' => array('driver' => $config['db']['driver'],
'database' => array('driver' => Util\Configuration::read('db.driver'),
'queries' => Util\Debug::getSQLLogger()->queries)
);

View file

@ -21,12 +21,7 @@
namespace Volkszaehler\View;
interface ViewInterface {
public function addException(\Exception $e);
public function addDebug();
}
abstract class View implements ViewInterface {
abstract class View {
public $request;
protected $response;
@ -44,33 +39,6 @@ abstract class View implements ViewInterface {
set_error_handler(array($this, 'errorHandler'));
}
/*
* creates new view instance depending on the requested format
* @todo improve mapping
*/
public static function factory(Http\Request $request, Http\Response $response) {
$format = strtolower($request->getParameter('format'));
$controller = strtolower($request->getParameter('controller'));
if (in_array($format, array('png', 'jpg'))) {
$view = new JpGraph($request, $response, $format);
}
else {
if ($controller == 'data' && ($format == 'json' || $format == 'xml')) {
$controller = 'channel';
}
$viewClassName = 'Volkszaehler\View\\' . ucfirst($format) . '\\' . ucfirst($controller);
if (!(\Volkszaehler\Util\ClassLoader::classExists($viewClassName)) || !is_subclass_of($viewClassName, '\Volkszaehler\View\View')) {
throw new \InvalidArgumentException('\'' . $viewClassName . '\' is not a valid View');
}
$view = new $viewClassName($request, $response);
}
return $view;
}
/*
* error & exception handling
*/
@ -102,4 +70,12 @@ abstract class View implements ViewInterface {
$this->response->send();
}
public function addException(\Exception $e) {
echo $e;
}
public function addDebug() {
}
}

View file

@ -22,6 +22,8 @@
namespace Volkszaehler\View\Xml;
// TODO outdated
use Volkszaehler\Util;
abstract class Xml extends \Volkszaehler\View\View {
protected $xmlDoc;
@ -57,12 +59,10 @@ abstract class Xml extends \Volkszaehler\View\View {
}
public function addDebug() {
$config = \Volkszaehler\Util\Registry::get('config');
$xmlDebug = $this->xmlDoc->createElement('debug');
$xmlDebug->appendChild($this->xmlDoc->createElement('time', $this->getTime()));
$xmlDebug->appendChild($this->xmlDoc->createElement('database', $config['db']['driver']));
$xmlDebug->appendChild($this->xmlDoc->createElement('database', Util\Configuration::read('db.driver')));
// TODO add queries

View file

@ -25,15 +25,6 @@ $config['db']['user'] = 'volkszaehler';
$config['db']['password'] = '';
$config['db']['dbname'] = 'volkszaehler';
$config['passthru']['enabled'] = false;
$config['passthru']['url'] = 'http://volkszaehler.org/httplog/httplog.php?&passthru=yes';
$config['debug'] = false;
// insert configuration into registry
Registry::set('config', $config);
// unset registry from page context
unset($config);
?>

View file

@ -0,0 +1,33 @@
<?php
/*
* Copyright (c) 2010 by Justin Otherguy <justin@justinotherguy.org>
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License (either version 2 or
* version 3) as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*
* For more information on the GPL, please go to:
* http://www.gnu.org/copyleft/gpl.html
*/
use Volkszaehler\Util;
include '../../backend/lib/Util/Configuration.php';
echo '<pre>';
Util\Configuration::load('config_test');
var_dump(Util\Configuration::read());
Util\Configuration::store('config_test');
echo '</pre>';
?>

25
share/tests/test.php Normal file
View file

@ -0,0 +1,25 @@
<?php
/*
* That's the volkszaehler.org configuration file.
* Please take care of the following rules:
* - you are allowed to edit it by your own
* - anything else than the $config declaration
* will maybe be removed during the reconfiguration
* by the configuration parser!
* - only literals are allowed as parameters
* - expressions will be evaluated by the parser
* and saved as literals
*/
$config['db'] = array (
'driver' => 'pdo_mysql',
'host' => 'localhost',
'user' => 'volkszaehler',
'password' => '',
'dbname' => 'volkszaehler',
);
$config['debug'] = false;
?>

47
share/tools/install.php Normal file
View file

@ -0,0 +1,47 @@
<?php
/*
* Copyright (c) 2010 by Justin Otherguy <justin@justinotherguy.org>
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License (either version 2 or
* version 3) as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*
* For more information on the GPL, please go to:
* http://www.gnu.org/copyleft/gpl.html
*/
?>
<?= '<?xml version="1.0"' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>volkszaehler.org installer</title>
</head>
<body>
<?php
switch (@$_GET['step']) {
case '1':
echo 'bla';
break;
default:
echo '<p>welcome to the installation of your volkszaehler backend!</p>
<p>lets proceed with the <a href="?step=1">next step</a></p>';
}
?>
</body>
</html>