REWROTE: SOLVED
Hi there,
I currently worked on a simple application with a database, a bunch of controllers, views and a model class.
I coded the controllers and inserted the db connections directly
E.g.
Each controller method has his own PDO to connect to a specific database+table.
I refactored this because I had too many active PDOs per 1 controller, so I started to code the model class.
A short information: The model class is once accessed by a controller, when the controller is called.
Once the model object is constructed it is available through the whole controller, and you can pass custom request to it.
E.g. getUserById => Gets the User from the current controller table with the id "xy".
Now that I finally finished the model class and added my PDO class to the model:
Everytime I want to access any of my controllers, my FireFox asks me where to safe the empty "test.php" (test.php is my index file).
Restarting Apache2 / PHP / MySQL did not work, if I remove a certain part of my code, there is no error, but this part is essential. ;)
model.php
class Model extends db_pdo_adapter{
public function __construct($name)
{
$name = strtolower($name);
$this->dbh = parent::connect(Model::ATTR_HOST, Model::ATTR_USR, Model::ATTR_PASSWD, Model::ATTR_DB);
$this->name = $name.'s';
//$this->ATTR_TBL = $this->name;
}
public function __call($name,$values)
{
$string = preg_replace('/^get/','',$name);
$string = strtolower($string);
$by = preg_split('/by/',$string);
$by = strtolower($by[1]);
return $this->get($string, $by, $values); // when I remove this part no empty file is served.
}
public function get($item, $by, $conditions) // single item if is_no_array
{
if($item = preg_replace('/$s/','',$this->name))
{
$item = '*';
}
//if(count($conditions) <= 1)
//{
$query = 'SELECT ' . $item . ' FROM ' . $this->name . ' WHERE ' . $by . ' = :' . $by . '';
$pname = ':'.$by;
//}
$this->dbh->getStatement($query);
$this->dbh->bindParam($pname,$conditions[0]); // ->dbh-> also was missing
$this->dbh->exec();
return ($this->dbh->fetchAll());
}
}
Extract of test.php
header('Content-Type: text/html;');
$time_start = microtime(true);
include_once('db/model.php');
//include_once('village.php');
//include_once('player.php');
include_once('building.php');
//$village = Village::getVillage('12');
//$player = Player::getPlayer('423');
//$data = array('name' => 'peter','password' => 'nopasswd','email' => 'peter#hasnomail.org');
//$player->newPlayer($data);
//print_r($village->attr);
//print_r($player->playerObj);
//include('interface.phtml');
//var_dump($_SERVER);
//print_r($village);
//print_r($player);
echo '<br />';
var_dump(Building::getBuilding('321'));
Extract of the building.php (controller)
class Building{
private function __construct($id,$village = NULL)
{
$this->model = new Model(__CLASS__);
$model = $this->model;
$this->buildingObj = $model->getBuildingById($id);
}
public function getBuilding($id,$village = NULL)
{
return (new Building($id));
}
}
I found the problem:
I have to extend the Model class with the PDO adapter, I do not know why, but this was the problem.
Related
I know this answer has been answered already but it seems like I couldn't understand anything in my case (I'm still very new to web developing, learning frontend since this october and I jumped onto php at the start of this month).
My function is supposed to check if user is logged in and I couldn't understand the answers I read for my problem cause it seems like in the answers I found, the functions weren't custom? I might be wrong and I hope if I am, you can laugh about it.
So, to get a bit of context, I encounter this error when I try to call the function "check_logged_in()" in one of my controllers, this controller in question (it's called Upload) extends the main Controller from my core folder (and I triple checked if my init file has required it so I can use it globally).
Then I call a custom function from the main controller to load models if needed (in this case, I need to load my user model to get access to the "check_logged_in()" function since it's written there).
And this is where thing happen. I'll provide a bit of code so you guys can understand what I'm saying.
The Upload controller
<?php
class Upload extends Controller
{
function index()
{
header("location:" . ROOT . "upload/image");
die;
}
function image()
{
$user = $this->loadModel("user");
if(!$result = $user->check_logged_in()){
header("location:" . ROOT . "login");
die;
}
$data['page_title'] = "Upload";
$this->view("upload", $data);
}
}
The main Controller
<?php
class Controller
{
protected function view($view, $data = [])
{
if (file_exists("../app/views/" . $view . ".php")) {
include "../app/views/" . $view . ".php";
} else {
include "../app/views/404.php";
}
}
protected function loadModel($model)
{
if (file_exists("../app/model/" . $model . ".php")) {
include "../app/model/" . $model . ".php";
return $model = new $model();
}
return false;
}
}
And the bit of code from the user model that is called
<?php
class User
{
function check_logged_in()
{
$DB = new Database();
if (isset($_SESSION['user_url'])) {
$arr['user_url'] = $_SESSION['user_url'];
$query = "select * from users where url_address = :user_url limit 1";
$data = $DB->read($query, $arr);
if (is_array($data)) {
//logged in
$_SESSION['user_name'] = $data[0]->username;
$_SESSION['user_url'] = $data[0]->url_address;
return true;
}
}
return false;
}
}
Thanks in advance for your time mates, I hope my noob self is clear enough for you to understand ^^'
Checking for the correct value of the $user variable should fix the error.
...
function image()
{
$user = $this->loadModel("user");
if(!$user || !$user->check_logged_in()){ // Here
header("location:" . ROOT . "login");
die;
}
$data['page_title'] = "Upload";
...
Then on $user is false condition will be
!false || !false->check_logged_in() which will lead to true.
Otherwise on $user is User:
!(object User) || !(object User)->check_logged_in() will call User::check_logged_in() method.
I would like to make the following function available throughout my routers.
public function getAll($request, $response, $args, $table, $prefix, $order, $PermRead) {
// retrieve all records
// WORKING... Security questions
// 1. First, check to make sure authenticated (via JSESSION_ID, etc.)
// 2. Automatically apply site_id to ALL queries
// 3. Apply sch_id to this query
// 4. Get permissions
$status = null;
$site_id = $sch_id = 1;
if (!$PermRead) {
$status = 403; // 403 Forbidden
} else {
$sql =
"SELECT * from " . $table .
" WHERE " . $prefix . "_site_id = " . $site_id .
" AND " . $prefix . "_sch_id = " . $sch_id .
" AND " . $prefix . "_deleted_timestamp IS NULL " .
" ORDER BY " . $order;
$rows = $this->dbw->run($sql);
}
if (!$status) {
$status = 200; // 200 OK
}
return $response->withStatus($status)->withJson($rows);
}
However, I get the following error: Fatal error: Using $this when not in object context in C:\Wamp\www\ravine\server\src\routes.php on line 26
How should I make this function available so that I can call it inside my route, like this:
// retrieve all classroom records
$app->get('/classrooms', function ($request, $response, $args) {
$PermRead = true; // check permissions
return getAll($request, $response, $args, "classroom", "room", "room_name", $PermRead);
});
Implementation of Werner's suggestion to use the application's container:
I created a class called Common in /lib/common.php:
<?php
namespace lib;
use Interop\Container\ContainerInterface;
class Common {
protected $ci;
private $site_id = 1;
//Constructor
public function __construct(ContainerInterface $ci) {
$this->ci = $ci;
}
public function getAll($table, $prefix, $order, $PermRead) {
// retrieve all records
// WORKING... Security questions
// 1. First, check to make sure authenticated (via JSESSION_ID, etc.)
// 2. Automatically apply site_id to ALL queries
// 3. Apply sch_id to this query
// 4. Get permissions
$status = null;
$site_id = $sch_id = 1;
if (!$PermRead) {
$status = 403; // 403 Forbidden
} else {
$sql =
"SELECT * from " . $table .
" WHERE " . $prefix . "_site_id = " . $site_id .
" ORDER BY " . $order;
$rows = $this->ci->dbh->run($sql);
$this->ci->response->withJson($rows);
}
if (!$status) {
$status = 200; // 200 OK
}
return $this->ci->response->withStatus($status);
}
}
Then, I added the class to /src/dependencies.php
<?php
require __DIR__ . '/../lib/common.php';
$container = $app->getContainer();
// common router functions
$container['common'] = function ($c) {
$common = new lib\Common($c);
return $common;
};
Now, within my individual router files, I'm able to call the common function like this in /routers/classroom.router.php:
// retrieve all classroom records
$app->get('/classrooms', function ($request, $response, $args) {
$PermRead = true; // check permissions
return $this->common->getAll("classroom", "room", "room_name", $PermRead);
});
The container carries $request, $response and $args (and other functions).
I would suggest making use of application containers to simplify your application structure. Slim 3 has been designed to work well with application containers.
Pass the container to your class method - you will then have the request and response objects available via the (shared) container, since Slim assigns those (request and response) to the container object automatically.
You can even add/assign your database connection (and whatever else you want to make available to other classes) to the container, then you only need to pass the same container to all functions that require database functionality.
The idea is that you can write classes that can be re-used in other projects, even if you decide to use something different than Slim next time. As long as the framework uses application containers, you can probably re-use your classes.
Eg: In you index.php
$container = $app->getContainer();
$container['db'] = $myDbConnection;
$container['request'] and $container['response'] are assigned automatically by the framework.
E.g MyClass.php
use Interop\Container\ContainerInterface;
class MyClass {
public function getAll(ContainerInterface $container) {
// ...
$myDb = $container['db'];
// ... do DB stuff
$response = $container['response'];
return $response->withStatus($status)->withJson($rows);
}
}
$this is not available in your function, the easiest way would be to just add it as an parameter.
Something like:
public function getAll($request, $response, $args, $table, $prefix, $order, $PermRead, $app) {
[..]
$app->dbw->...;
Then call it with $this in the parameter
return getAll($request, $response, $args, "classroom", "room", "room_name", $PermRead, $this);
I am trying to make a base class ... tiny framework if you will just for practice
So I start with example of child class because it has less code !!
class User extends Base {
public $id ;
public $username ;
public $email ;
public $password ;
function __construct(){
$this->table_name = 'users';
$this->set_cols(get_class_vars('User'));
}
}
$u = new User;
$u->username = 'jason';
$u->email = 'j#gmail.com';
$u->insert();
Here is my Base class
class Base {
protected $table_name ;
protected $table_columns ;
protected function set_cols($cols){
unset($cols['table_name']);
unset($cols['table_columns']);
$this->table_columns = array_keys($cols);
}
public function insert(){
$colums = $values = array();
foreach($this->table_columns as $col )
{
if(!$this->$col) continue ;
$values[] = $this->$col ;
$colums[] = $col ;
}
$values = implode(',' , $values);
$colums = implode(',' , $colums);
echo $sql = "INSTER INTO ".$this->table_name ." ($colums)
VALUES ($values) ";
}
}
Here is the problem , I want to make filter or get method (basically reading from database) static and then return an array of objects from database data
class Base{
static function filter($conditions =array()){
$query_condition = $conditions ; // some function to convert array to sql string
$query_result = "SELECT * FROM ".$this->table_name ." WHERE $query_condition ";
$export = array();
$class = get_called_class();
foreach($query_result as $q )
{
$obj = new $class;
foreach($this->table_columns as $col )
$obj->$col = $q[$col];
$export[] = $obj;
}
return $export;
}
}
$users = User::filter(['username'=>'jason' , 'email'=>'j#gmail.com']);
Here is the problem , with filter as static function __construct in User class will not get called and table_columns, table_name will be empty
also in the filter method I can't access them anyway because they are not static ... I can make a dummy User object in the filter method and solve this problems but somehow it doesn't feel right
Basically I have a design problem any suggestion is welcomed
The problem is that the static object is not really "created" when you run statically.
If you want the constructor to run, but still in a static sort of way, you need a "singleton". This is where the object is created once and then you can re-use. You can mix this technique in a static and non-static way (as you're actually creating a "global" object that can be shared).
An example is
class Singleton {
private static $instance;
public static function getInstance() {
if (null === static::$instance) {
self::$instance = new static();
}
return self::$instance;
}
}
$obj = Singleton::getInstance();
Each time this gets the same instance and remembers state from before.
If you want to keep your code base with as few changes as possible, you can create yourself an "initialized" variable statically - you just need to remember to call it in each and every function. While it sounds great, it's even worse than a Singleton as it still remembers state AND you need to remember the init each time. You can, however, use this mixed with static and non-static calls.
class notASingletonHonest {
private static $initialized = false;
private static function initialize() {
if (!self::$initialized) {
self::$initialized = true;
// Run construction stuff...
}
}
public static function functionA() {
self::$initialize();
// Do stuff
}
public static function functionB() {
self::$initialize();
// Do other stuff
}
}
But read a bit before you settle on a structure. The first is far better than the second, but even then if you do use it, ensure that your singleton classes can genuinely be ran at any time without reliance on previous state.
Because both classes remember state, there are many code purists that warn you not to use singletons. You are essentially creating a global variable that can be manipulated without control from anywhere. (Disclaimer - I use singletons, I use a mixture of any techniques required for the job.)
Google "php Singleton" for a range of opinions and more examples or where/where not to use them.
I agree with a lot of your premises in your code and design. First - User should be a non static class. Second - Base base should have a static function that acts a factory for User objects.
Lets focus on this part of your code inside the filter method
1 $query_result = "SELECT * FROM ".$this->table_name ." WHERE $query_condition ";
2 $export = array();
3
4
5 $class = get_called_class();
6 foreach($query_result as $q )
7 {
8 $obj = new $class;
9
10 foreach($this->table_columns as $col )
11 $obj->$col = $q[$col];
12
13 $export[] = $obj;
14
15 }
The issue is that lines 1 and 10 are trying to use this and you want to know the best way to avoid it.
The first change I would make is to change protected $table_name; to const TABLE_NAME like in this comment in the php docs http://php.net/manual/en/language.oop5.constants.php#104260. If you need table_name to be a changeable variable, that is the sign of bad design. This will allow you change line 1 to:
$class = get_called_class()
$query_result = "SELECT * FROM ". $class::TABLE_NAME . "WHERE $query_condition";
To solve the problem in line 10 - I believe you have two good options.
Option 1 - Constructor:
You can rewrite your constructor to take a 2nd optional parameter that would be an array. Your constructor would then assign all the values of the array. You then rewrite your for loop (lines 6 to 15) to:
foreach($query_result as $q)
{
$export[] = new $class($q);
}
And change your constructor to:
function __construct($vals = array()){
$columns = get_class_vars('User');
$this->set_cols($columns);
foreach($columns as $col)
{
if (isset($vals[$col])) {
$this->$col = $vals[$col];
}
}
}
Option 2 - Magic __set
This would be similar to making each property public, but instead of direct access to the properties they would first run through a function you have control over.
This solution requires only adding a single function to your Base class and a small change to your current loop
public function __set($prop, $value)
{
if (property_exists($this, $prop)) {
$this->$prop = $value;
}
}
and then change line 10-11 above to:
foreach($q as $col => $val) {
$obj->$col = $val
}
Generally it is a good idea to seperate the logic of storing and retrieving the data and the structure of the data itself in two seperate classes. A 'Repository' and a 'Model'. This makes your code cleaner, and also fixes this issue.
Of course you can implement this structure in many ways, but something like this would be a great starting point:
class Repository{
private $modelClass;
public function __construct($modelClass)
{
$this->modelClass = $modelClass;
}
public function get($id)
{
// Retrieve entity by ID
$modelClass = $this->modelClass;
return new $$modelClass();
}
public function save(ModelInterface $model)
{
$data = $model->getData();
// Persist data to the database;
}
}
interface ModelInterface
{
public function getData();
}
class User implements ModelInterface;
{
public int $userId;
public string $userName;
public function getData()
{
return [
"userId" => $userId,
"userName" => $userName
];
}
}
$userRepository = new Repository('User');
$user = $userRepository->get(2);
echo $user->userName; // Prints out the username
Good luck!
I don't think there is anything inherently wrong with your approach. That said, this is the way I would do it:
final class User extends Base {
public $id ;
public $username ;
public $email ;
public $password ;
protected static $_table_name = 'users';
protected static $_table_columns;
public static function getTableColumns(){
if( !self::$_table_columns ){
//cache this on the first call
self::$_table_columns = self::_set_cols( get_class_vars('User') );
}
return self::$_table_columns;
}
public static function getTableName(){
return self::$_table_name;
}
protected static function _set_cols($cols){
unset($cols['_table_name']);
unset($cols['_table_columns']);
return array_keys($cols);
}
}
$u = new User;
$u->username = 'jason';
$u->email = 'j#gmail.com';
$u->insert();
And then the base class, we can use Late Static Binding here static instead of self.
abstract class Base {
abstract static function getTableName();
abstract static function getTableColumns();
public function insert(){
$colums = $values = array();
foreach( static::getTableColumns() as $col ){
if(!$this->$col) continue ;
$values[] = $this->$col ;
$colums[] = $col ;
}
$values = implode(',' , $values);
$colums = implode(',' , $colums);
echo $sql = "INSERT INTO ". static::getTableName() ." ($colums) VALUES ($values) ";
}
static function filter($conditions =array()){
$query_condition = $conditions ; // some function to convert array to sql string
$query_result = "SELECT * FROM ".static::getTableName() ." WHERE $query_condition ";
$export = array();
$columns = static::getTableColumns(); //no need to call this in the loop
$class = get_called_class();
foreach($query_result as $q ){
$obj = new $class;
foreach( $columns as $col ){
$obj->$col = $q[$col];
}
$export[] = $obj;
}
return $export;
}
}
Now on the surface this seems trivial but consider this:
class User extends Base {
public $id ;
public $username ;
public $email ;
public $password ;
final public static function getTableName(){
return 'users';
}
final public static function getTableColumns(){
return [
'id',
'username',
'email',
'password'
];
}
}
Here we have a completely different implementation of those methods from the first Users class. So what we have done is force implementation of these values in the child classes where it belongs.
Also, by using methods instead of properties we have a place to put custom logic for those values. This can be as simple as returning an array or getting the defined properties and filtering a few of them out. We can also access them outside of the class ( proper like ) if we need them for some other reason.
So overall you weren't that far off, you just needed to use static Late Static Binding, and methods instead of properties.
http://php.net/manual/en/language.oop5.late-static-bindings.php
-Notes-
you also spelled Insert wrong INSTER.
I also put _ in front of protected / private stuff, just something I like to do.
final is optional but you may want to use static instead of self if you intend to extend the child class further.
the filter method, needs some work yet as you have some array to string conversion there and what not.
I am having trouble on accessing to an instance of a singleton class in PHP MVC. Basically the MVC looks like
First of all I have included and instantiated objects in the init.php as
// include objects
include('app/Database.php');
include('app/models/m_template.php');
include('app/models/m_categories.php');
// create objects
$tdatabase = new Database();
$Template = new Template();
$Categories = new Categories();
and in m_categories.php I tried to use the $tdatabase object as:
<?php
class Categories {
private $db_table = 'category';
function __construct() { }
public function get_categories() {
$data = array();
$res = $tdatabase->query("SELECT * FROM " . $this->db_table . " ORDER BY name");
foreach ($res as $dataRow):
$data[] = array('id' => $dataRow['id'],
'name' => $dataRow['name'],
'img' => $dataRow['img'],
'alt' => $dataRow['alt'],
);
endforeach;
return $data;
}
}
and finally in index.php I have:
<?php
include('app/init.php');
echo '<pre>';
print_r($Categories->get_categories());
echo '</pre>';
but I am getting following errors:
can you please let me know why this is happening and how I can fix this?
Update 1:
Update 2
Categories object doesn't have any information about outside variables. You sholud pass Database object to your Categories object e.g. by constructor or by method parameter.
init.php
$tdatabase = new Database();
$Template = new Template();
$Categories = new Categories($tdatabase);
m_categories.php
class Categories {
protected $database;
private $db_table = 'category';
function __construct($database) {
$this->database = $database;
}
public function get_categories() {
$data = array();
$res = $this->database->query("SELECT * FROM " . $this->db_table . " ORDER BY name");
// (...)
}
your variable tdatabase is out of scope. you either need to pass it into the function, or set it as a class member variable in the constructor or via a setter
i.e.
public function get_categories(Database $tdatabase) {
$data = array();
$res = $tdatabase->query("SELECT * FROM " . $this->db_table . " ORDER BY name");
I often see code like this though, and I (almost) always recommend, especially for new projects that you use the model/mapper pattern because it is more easily extensible and is more maintainable. See here for an example:
OOP PHP PDO My First Project , Am I doing right?
I am using a Spotify library called MetaTune and was able to do this easily in CodeIgniter but with Yii there have been some teething issues however currently it has started saying:
Fatal error: Call to undefined method stdClass::searchTrack() in ....public_html/Yii/news/protected/controllers/NewsController.php on line 67
Howevever, the the function is there. The files in this library all have a .class.php suffix (e.g. MetaTune.class.php) and the libray files are all stored in:
yii/application/protected/vendors/Metatune
With Codeigniter I made an additional spotify.php outside of the folder and autoloaded that to my controller, but im not sure if this is necessary.
I have loaded it in my config.php with:
'import'=>array(
'application.models.*',
'application.components.*',
'application.vendors.metatune.*',
),
Here is the Controller code:
public function actionView($id)
{
$model=$this->loadModel($id);
$spotify = MetaTune::getSomething();
$hello = $model->title;
Yii::import('application.vendors.metatune.MetaTune');
$spotify->autoAddTracksToPlayButton = true; // Will add all searches for tracks into a list.
$spotify->playButtonHeight = 330; // For viewing the entire playlist
$spotify->playButtonTheme = "dark"; // Changing theme
$spotify->playButtonView = "coverart"; // Changing view
try
{
$tracks = $spotify->searchTrack($hello);
$tracks = $spotify->getPlayButtonAutoGenerated($hello);
}
catch (MetaTuneException $ex)
{
die("<pre>Error\n" . $ex . "</pre>");
}
$song = 'tracks';
$this->render('view',array(
'model'=>$this->loadModel($id),
));
}
Please also see the code below where it has a function called getInstance which doesnt work well with Yii for some reason and Im not sure if I can change this as I used this to import MetaTune into the CodeIgniter controller without any issues.
Just a part of the MetaTune.class.php code:
Yii::import('application.vendors.metatune.Artist');
Yii::import('application.vendors.metatune.Album');
Yii::import('application.vendors.metatune.Track');
Yii::import('application.vendors.metatune.CacheRequest');
Yii::import('application.vendors.metatune.MBSimpleXMLElement');
Yii::import('application.vendors.metatune.SpotifyItem');
Yii::import('application.vendors.metatune.MetaTuneException');
....
class MetaTune {
const CACHE_DIR = 'application/vendors/metatune/cache/'; // Cache directory (must be writable) relative to this file
const USE_CACHE = false; // Should caching be activated?
const CACHE_PREFIX = "METATUNE_CACHE_"; // prefix for cache-files.
const SERVICE_BASE_URL_SEARCH = "http://ws.spotify.com/search/1/";
const SERVICE_BASE_URL_LOOKUP = "http://ws.spotify.com/lookup/1/";
const PLAYBUTTON_BASE_URL = "https://embed.spotify.com/?uri=";
public $autoAddTracksToPlayButton = false;
private $list = array();
// Holds instance
private static $instance;
.....
public static function getSomething()
{
if (!isset(self::$instance))
{
$class = __CLASS__;
self::$instance = new $class;
}
return self::$instance;
}
.....
public function searchTrack($name, $page = 1)
{
$url = self::SERVICE_BASE_URL_SEARCH . "track?q=" . $this->translateString($name) .
$this->addPageSuffix($page);
$contents = $this->requestContent($url);
$xml = new MBSimpleXMLElement($contents);
$tracks = array();
foreach ($xml->track as $track)
{
$tracks[] = $this->extractTrackInfo($track);
}
if ($this->autoAddTracksToPlayButton) {
$this->appendTracksToTrackList($tracks);
}
return $tracks;
}
If you have any suggestions I would be most grateful. Thanks.
You didn't initialize $spotify anywhere, and php made it into stdClass by default, since you were assigning values like to member properties using that variable, but it failed when you tried calling unexisting method on it.
Solution: initialise it before you use it
$spotify = MetaTune::getInstance();