Im trying to extend https://github.com/Quadra-Digital/silverstripe-schema to be able to better handle nested schemas. I've managed to get it working all though it breaks one of the very useful features Dynamic Values.
It currently uses a DataObject called RelatedObject to map its related object which works fine when there is only one level of nesting. However once you get further into it and have SchemaInstances nested upon SchemaPropertys you lose the overarching owner, and the RelatedObject becomes the Property that the Instance is nested under.
class SchemaInstance extends DataObject {
private static $db = [
'ParentClass' => 'Varchar(255)'
];
private static $has_one = [
'RelatedObject' => 'DataObject',
'Schema' => 'Schema',
];
private static $has_many = [
'Properties' => 'SchemaProperty'
];
class SchemaProperty extends DataObject {
private static $db = [
'Title' => 'Varchar(255)',
'ValueStatic' => 'Varchar(255)',
'ValueDynamic' => 'Varchar(255)'
];
private static $has_one = [
'ParentSchema' => 'SchemaInstance'
];
private static $has_many = [
'NestedSchemas' => 'SchemaInstance'
];
This is what is currently saved when I created the nested schema instance.
52 SchemaInstance 2018-03-04 04:28:38 2018-03-04 04:28:38 0 File 8 0 96 SchemaProperty
I need it to look like this
52 SchemaInstance 2018-03-04 04:28:38 2018-03-04 04:28:38 0 File 8 0 96 *ActualParentClass*
I have tried getting the owner in the SchemaObjectExtension (Which works fine, I just can't figure out how to use that rather than the default)
class SchemaObjectExtension extends DataExtension {
private static $has_many = [
'SchemaInstances' => 'SchemaInstance.RelatedObject',
];
I'd be happy with just being able to save the owner class to the db somehow and the reference that. I've tried getting it from the DataObject but it gives a
"getOwner() is not a method on Dataobject" error.
Thanks for any help
Related
I have two DataObjects on Silverstripe 4.
First a Quiz whith a has_many-relationships with the questions for that Quiz.
use SilverStripe\ORM\DataObject;
use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\TextField;
use SilverStripe\Forms\DateField;
use SilverStripe\Forms\LiteralField;
use SilverStripe\Forms\CheckboxField;
class Quiz extends DataObject {
private static $db = [
"Name" => "Varchar(200)",
"bis" => "Date()",
"aktiv" => "Boolean",
"Mail" => "Boolean",
"MailText" => "Text"
];
private static $has_one = [
];
private static $has_many = [
"Fragen" => Quiz_Fragen::class
];
The code for the questions.
use SilverStripe\ORM\DataObject;
use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\TextField;
use SilverStripe\Forms\TextareaField;
class Quiz_Fragen extends DataObject {
private static $db = [
"Frage" => "HTMLText",
"Art" => "Enum(array('auswählen','MC','Text','Check'))",
"OP1" => "Varchar(400)",
"OP2" => "Varchar(400)",
"OP3" => "Varchar(400)",
"OP4" => "Varchar(400)",
"Foul" => "Varchar(50)",
"Team" => "Enum(array('A','B'))",
"Punkte" => "Varchar(200)",
"Down" => "Enum(array('1','2','3','4','Try','FK'))",
"Pos" => "Varchar(25)",
"Distanz" => "Varchar(2)",
"Uhr" => "Enum(array('Snap','Ballfreigabe','Down ohne Zeit','keine','läuft'))",
"Sonstiges" => "Varchar(50)",
"Antwort" => "HTMLText",
"SortOrder" => "Int",
"Grund" => "HTMLText",
"Pkt" =>"Enum(array('1','2','3','4','5','6','7','8','9','10','11','12'))"
];
private static $has_one = [
"Quiz" => Quiz::class
];
private static $has_many = [
];
In Silverstripe 3 I chose a entry of the dataobject quiz an had a link on the top to see the questions related to the chosen quiz. I miss this link in Silverstripe 4. I'm sure I'm just missing a little thing. But I can't find a solution.
It was the FieldList.
On the dataobject Quiz I added the fields this way.
$fields = FieldList::create(
TextField::create('Name','Name des Quiz'),
DateField::create('bis','Quiz läuft bis'),
LiteralField::create("Text", "Vor der Aktivierung die Fragen eintragen. Sobald aktiviert wird bekommen die Benutzer eine E-Mail.<br /><br />"),
CheckboxField::create('aktiv', 'Quiz aktivieren')
);
When I use the old way of my SS 3 version. It works. So the problem is solved.
Are behavior related configurations accessible? In this specific case, a behavior was attached to a table. I would like to know if it's possible to get the fields property in some way, later on in code?
<?php
class MyRandomTable extends Table
{
public function initialize(array $config)
{
parent::initialize($config);
...
// Add Cipher behavior
$this->addBehavior('CipherBehavior.Cipher', [
'fields' => [
'original' => 'string',
'changed' => 'string',
]
]);
}
...
}
?>
If I load the table and dump the content, I do not see the behavior listed:
$table = TableRegistry::get('MyRandomTable');
var_dump($table);
Partial dump content:
protected '_behaviors' =>
object(Cake\ORM\BehaviorRegistry)[170]
protected '_table' =>
&object(Cake\ORM\Table)[172]
protected '_methodMap' =>
array (size=0)
empty
protected '_finderMap' =>
array (size=0)
empty
protected '_loaded' =>
array (size=0)
empty
protected '_eventManager' =>
object(Cake\Event\EventManager)[165]
protected '_listeners' =>
array (size=0)
...
protected '_isGlobal' => boolean false
protected '_eventList' => null
protected '_trackEvents' => boolean false
protected '_eventClass' => string '\Cake\Event\Event' (length=17)
What I'd like to do, in a controller, is to get the fields and pass those on to a view.
Edit #1
Using CakePHP v3.3.16
Edit #2
I'm seeing behavior information as I had missed the plugin prefix when loading the table:
$table = TableRegistry::get('PluginName.MyRandomTable');
Shows:
protected '_behaviors' =>
object(Cake\ORM\BehaviorRegistry)[143]
protected '_table' =>
&object(PluginName\Model\Table\MyRandomTable)[94]
protected '_methodMap' =>
array (size=4)
'timestamp' =>
array (size=2)
...
'touch' =>
array (size=2)
...
'encrypt' =>
array (size=2)
...
'decrypt' =>
array (size=2)
...
protected '_finderMap' =>
array (size=0)
empty
protected '_loaded' =>
array (size=2)
'Timestamp' =>
object(Cake\ORM\Behavior\TimestampBehavior)[181]
...
'Cipher' =>
object(CipherBehavior\Model\Behavior\CipherBehavior)[192]
...
protected '_eventManager' =>
object(Cake\Event\EventManager)[175]
protected '_listeners' =>
array (size=4)
...
protected '_isGlobal' => boolean false
protected '_eventList' => null
protected '_trackEvents' => boolean false
protected '_eventClass' => string '\Cake\Event\Event' (length=17)
First of all your table class file is incorrect, it needs a namespace, otherwise it cannot be found, and you'll end up with an instance of \Cake\ORM\Table (a so called auto/generic-table) instead of concrete subclass thereof, hence your behavior is missing.
That being said, it depends on how the behavior has been programmed. If it follows the default configuration pattern, then you can access the configuration via its config() or getConfig() (as of CakePHP 3.4) methods.
Of course you have to access the behavior, not just the table class to which it is attached. This is done using the behavior registry, which is available via the Table::behaviors() method:
$fields = $table->behaviors()->get('Cipher')->config('fields');
See also
Cookbook > Database Access & ORM > Behaviors > Accessing Loaded Behaviors
Cookbook > Database Access & ORM > Behaviors > Re-configuring Loaded Behaviors
API > \Cake\ORM\BehaviorRegistry
API > \Cake\ORM\Behavior
Cookbook > Configuration > Disabling Generic Tables
You can get Column names of a table by schema()->columns().
Example -
$getColumnArray = $this->Users->schema()->columns();//return Users Table Colums Name Array
$getColumnArray = $this->Users->associations()->keys()//return Users assocation table key
Pulling my hair out over some weird behaviour.
Essentially I've got a Class that constructs a GuzzleHttp\Client and a custom object of organization data, like so:
// Config set-up
$config = [
'ex1' => [
'api_key' => getenv('EX1_API_KEY'),
'org_id' => getenv('EX1_ORG_ID'),
],
'ex2' => [
'api_key' => getenv('EX2_API_KEY'),
'org_id' => getenv('EX2_ORG_ID'),
],
'ex3' => [
'api_key' => getenv('EX3_API_KEY'),
'org_id' => getenv('EX3_ORG_ID'),
],
];
// Initialize adapters
$ex1 = new Adapter($config['ex1']);
$ex2 = new Adapter($config['ex2']);
$ex3 = new Adapter($config['ex3']);
Which is all a-okay, until they finish their construction with $this->org = $org, which overwrites all of them with the same $org, in this line inside the constructor:
// Construct connected org
$org = Organization::get($this, $args['org_id']);
$this->org = $org;
The frustrating part in all of this is if I assign a property of that org instead of the whole thing, each item comes through unique (e.g. $this->org = $org->name).
I have a feeling this has to do with my Organization class, but I don't know where to start debugging this. Can provide more code/context on request, but the entire code-base is on GitHub.
I structured my abstract Resource class as a Singleton pattern. Because of this, I could not have more than one instance at a time (thus, Singleton) and as such was just changing the properties of the same instance each time.
I'm using php.activerecord, and I am trying to link tables together. I'm not using their structure, but php.activerecord assumes I am, so it doesn't always work. I'm trying to use it on an already made app, so I can't change the database.
I learned from my previous question - Model association with custom table and key names - that I need to be as explicit as possible with the primary_key and foreign_key fields.
I'm having issues now using has_many through. I keep getting NULL, and I have no idea why.
So, here's a scenario: I have 3 tables, contacts, contactPrefs, and preferences. Those tables are as follows
contacts
--------
contactID
name
status
contactPrefs
------------
contactID
prefID
prefValue
preferences
-----------
prefID
name
description
Each contact has multiple contactPrefs. Each contactPrefs has one preferences. I tried to use has_many to get this working, but it's not. Here are my models:
Contacts.php:
<?php
class Contact extends ActiveRecord\Model {
static $primary_key = 'contactID';
static $has_many = array(
array(
'prefs',
'foreign_key' => 'contactid',
'primary_key' => 'contactid',
'class_name' => 'ContactPref'
),
array(
'preferences',
'foreign_key' => 'prefid',
'primary_key' => 'prefid',
'through' => 'prefs',
'class_name' => 'Preference'
)
);
}
ContactPref.php:
<?php
class ContactPref extends ActiveRecord\Model {
static $table_name = 'contactPrefs';
static $belongs_to = array(
array(
'contact',
'foreign_key' => 'contactid',
'primary_key' => 'contactid'
),
array(
'preference',
'foreign_key' => 'prefid',
'primary_key' => 'prefid'
)
);
}
Preference.php:
<?php
class Preference extends ActiveRecord\Model {
static $primary_key = 'prefID';
static $has_many = array(
array(
'prefs',
'foreign_key' => 'prefid',
'primary_key' => 'prefid',
'class_name' => 'ContactPref'
)
);
}
According to the docs, I now should be able to the following:
<?php
var_dump(Contact::find(1234)->preference);
I cannot. I get NULL. Oddly, I can do this:
<?php
var_dump(Contact::find(1234)->prefs[0]->preference);
That works correctly. But, shouldn't I be able to access the preference object directly through the contact object? Am I misunderstanding the docs (they aren't the greatest, in my opinion)? Am I doing something wrong?
First you are reading the docs with a small flaw. In the docs you are shown:
$order = Order::first();
# direct access to users
print_r($order->users); # will print an array of User object
Which you are already doing via Contact::find(1234)->prefs. Let me boil it down a bit
$contact = Contact::find(1234);
# direct access to prefs
print_r($contact->prefs); # will print an array of ContactPref object
Second, what you actually want is undefined. What should Contact::find(1234)->preference actually do? Return the preference of the first ContactPref? Return an array of Preference objects?
I feel like offering both:
<?php
class Contact extends ActiveRecord\Model {
static $primary_key = 'contactID';
static $has_many = array(
array(
'prefs',
'foreign_key' => 'contactid',
'primary_key' => 'contactid',
'class_name' => 'ContactPref'
),
array(
'preferences',
'foreign_key' => 'prefid',
'primary_key' => 'prefid',
'through' => 'prefs',
'class_name' => 'Preference'
)
);
public function get_preference() {
return isset($this->prefs[0])
? $this->prefs[0]->preference
: null
;
}
public function get_preferences() {
$preference=array();
foreach($this->prefs as $pref) {
$preference[]=$pref;
}
return $preference;
}
}
Let me explain a little bit what I have done. The ActiveRecord\Model class has a __get($name) function that looks for another function called get_$name, where $name in your case is preference (for the first result) and preference (for the entire collection). This means you can do Contact::find(1234)->preference which would be the same as doing Contact::find(1234)->prefs[0]->preference (but safer, due to the check) and Contact::find(1234)->preferences to get the entire collection of preferences.
This can be made better or optimized in numerous ways, so please don't take it as it is, but do try and adapt it to your specific situation.
For example you can either use the id of the preference as an index in the array or either not force a load of more data from ContactPrefs than the ones you are going to use and try a more intricate query to get the preference objects that you specifically need.
If I find a better implementation by getting through to work in the relationship definition, I'll return. But seeing the Unit Tests for active record, I'm skeptical.
There are several things that look strange, so it's not easy to come to a "this will fix it" for you, but this is an issue at least:
Fieldnames should always be lower-case in phpactiverecord. SQL doesn't mind it either way (not that table names ARE case-sensitive, but column names aren't). So make this:
static $primary_key = 'contactID';
into
static $primary_key = 'contactid';
The connections // find commands can be used in SQL, in which case it doesn't really matter how your key-string is 'cased', so some stuff works. But if the connection goes trough the inner-workings of phpmyadmin, it will fail. So check out this contactID but also the prefID.
Again, this goes only for COLUMN names, so don't go changing classnames or table-names to lowercase.
(extra point: phpmyadmin has trouble with combined primary keys. So while it might be ugly, you could add an extra row to your contactprefs table (if you don't allready have it) called id, to make that table actually have something to work with. It wouldn't give you much trouble, and it would help the activerecord library a lot)
Try the following:
<?php
var_dump(Contact::find(1234)->preferences);
The documentation says that with a has_many relationship, it should be referenced by a plural (http://www.phpactiverecord.org/projects/main/wiki/Associations#has_many_through). The Contact::find(1234) returns a Contact object which has multiple contactPrefs with their each Preference. In addition, in your Contact model, you specify the has_many as preferences .
static $has_many = array(
array(
'prefs',
'foreign_key' => 'contactid',
'primary_key' => 'contactid',
'class_name' => 'ContactPref'
),
array(
'preferences',
'foreign_key' => 'prefid',
'primary_key' => 'prefid',
'through' => 'prefs',
'class_name' => 'Preference'
)
);
Edit Through Modification:
Try the following Contact model
<?php
class Contact extends ActiveRecord\Model {
static $primary_key = 'contactID';
static $has_many = array(
array(
'prefs',
'foreign_key' => 'contactid',
'class_name' => 'ContactPref'
),
array('preferences',
'through' => 'prefs',
'class_name' => 'Preference',
'primary_key' => 'prefID')
);
}
I have the following, for example:
class Model_User extends ORM {
protected $_rules = array(
'username' => array(
'not_empty' => NULL,
'min_length' => array(6),
'max_length' => array(250),
'regex' => array('/^[-\pL\pN_#.]++$/uD'),
),
'password' => array(
'not_empty' => NULL,
'min_length' => array(5),
'max_length' => array(30),
),
'password_confirm' => array(
'matches' => array('password'),
),
);
}
class Model_UserAdmin extends Model_User {
protected $_rules = array(
'username' => array(
'not_empty' => NULL,
'min_length' => array(6),
'max_length' => array(250),
'regex' => array('/^[-\pL\pN_#.]++$/uD'),
),
'password' => array(
'not_empty' => NULL,
'min_length' => array(5),
'max_length' => array(42),
),
);
}
In here, Model_UserAdmin extends Model_User and overrides the max length for password and removes the validation for password_confirm (this is not an actual case, but an example).
Is there a better way instead of redefining the entire $_rules property/array?
Use _initialize() instead of __construct($id) if you want to store your UserAdmin model in session (like Auth module does). Serialized ORM objects will not call __construct(), so part of your rules will lost. _initialize() method sets default values for model properties like table_name, relationships etc
protected function _initialize()
{
// redefine model rules
$this->_rules['password']['max_length'] = 42 ;
unset($this->_rules['password_confirm']) ;
// call parent method
parent::_initialize();
}
In the child's constructor you can probably overwrite or add array elements to $this->_rules, as it will already exist as soon as you create a Model_UserAdmin instance.
Specifically, in Model_UserAdmin don't define a protected $rules so it gets it from its parent, and then in the constructor:
$this->_rules['password']['max_length'] = 42 ;
unset($this->_rules['password_confirm']) ;
You can also add some sanity check right before to make sure those keys exist, in case you change them in Model_User and forget.
It's not exactly elegant but it should work. I do suppose you can create some wrapper functions around it (probably in a class ORM extends ORM_Core so they're available when you extend ORM) that modify the rules in a more formal way.
edit please look at biakaveron's answer for a tip on where to place the child rules (_initialize() instead of the constructor)