easyrdf
easyrdf copied to clipboard
EasyRdf_Graph::classForResource : how can we be more flexible ?
Hi,
During the EasyRdf_Collection implementation you added specific code in the EasyRdf_Graph classForResource method to handle optional rdf:List type:
// Parsers don't typically add a rdf:type to rdf:List, so we have to
// do a bit of 'inference' here using properties.
if ($uri == 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil' or
isset($this->index[$uri]['http://www.w3.org/1999/02/22-rdf-syntax-ns#first']) or
isset($this->index[$uri]['http://www.w3.org/1999/02/22-rdf-syntax-ns#rest'])
) {
return 'EasyRdf_Collection';
}
Today, we are implementing a SPIN parser using EasyRdf (https://github.com/conjecto/easyspinrdf) and we are facing the same issue : the type sp:TriplePattern is optional, if the element respect the sp:subject, sp:predicate and sp:object template, i need to map him to my specific class EasySpinRdf_Element_TriplePattern.
Do you think we can make evolve the typemapper to let him handle thoses "template based mapping" ?
Best regards,
Yes, I hate that hack that I added for rdf:List. Would be good to make it more generic.
A way of deciding on type based on pattern patching would be good - I did plan to add support for basic inferencing at some point - perhaps that would be better, although more complex?
I would like to implement this, but I'm unsure about the approach.
- expose a
TypeMapper::guesser($type, $callable)method, to specify an arbitrary function which retrieves the desidered type given the URI and the properties of the resourse. e.g.:
TypeMapper::guesser('EasyRdf\Collection', function($uri, $properties) {
if ($uri == 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil' or
isset($properties['http://www.w3.org/1999/02/22-rdf-syntax-ns#first']) or
isset($properties['http://www.w3.org/1999/02/22-rdf-syntax-ns#rest'])
) {
return true;
}
});
- or introduce a
TypeGuesserinterface (or abstract class) to wrap such a function, and enforce the signature of the custom identification method. e.g.:
class EnforceCollection implements EasyRdf\TypeGuesser
{
public function guess($uri, $properties)
{
if ($uri == 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil' or
isset($properties['http://www.w3.org/1999/02/22-rdf-syntax-ns#first']) or
isset($properties['http://www.w3.org/1999/02/22-rdf-syntax-ns#rest'])
) {
return 'EasyRdf\Collection';
}
}
}
TypeMapper::addGuesser(new EnforceCollection());
The first approach is more convenient but error prone, the second approach requires more code by the implementing user but can eventually be extended more (and eventually manage also the existing type mapping behaviour) to provides even more customizations levels.
Suggestions?
For me it looks an elegant solution would be to:
- Allow to set default TypeMapper class on a Graph (while looking into the EasyRdf code I can only see Graph calling TypeMapper and not TypeMapper calling Graph which suggests it's Graph which should be aware of your own TypeMapper and not the opposite).
- Introduce a TypeMapper interface so users can provide their own TypeMapper implementations without a need to commit to the EasyRdf so their own implementation works.
@zozlak
- why attach a TypeMapper to each Graph? Actually it is global, one for all, you can init it as you prefer once and still - if you really need it - alter his behaviour at any time
- that given, it is not possible to provide own implementation. Which may be a limit, but don't really get the point about doing so
- in any way, the only actual method provided by TypeMapper to select a PHP class is based on RDF type. Without introducing other methods (for which Graph is aware), having TypeMapper global or local for the Graph makes no difference
why attach a TypeMapper to each Graph
To make it even more flexbile and cleaner (once we decided to make it more flexible than it's now).
- In my approach you may not only alter type mapping but also (if you want) use different type mappings for different graphs. All in all enforcing all graphs to use exactly same type mapper is completely arbitrary and IMHO is a bad programming practice (btw same applies to the RdfNamespace).
- You can still keep the ability to adjust mappings for all graphs using a given type mapper implementation at once (see the example below).
- Of course you can still have a default implementation so users who don't care don't need to think about it (and the backward compatibility is assured).
- IMHO type mapping is a contract between a graph and a mapper. We can artificially push it down to a new layer (an interface between mapper and user) but it seems like unnecessary complication of the architecture to me.
I would propose something like (alternatively with a separate type mapper setter instead of constructor parameter; the myMapper::defineMapping() method body doesn't make much sense, it's there only to demonstrate all graphs can be affected at once):
interface EasyRdf\TypeMapperInterface {
public static function getClass(string $uri, array $properties): string;
}
class EasyRdf\Graph() {
private $mapper;
public function __construct($uri = null, $data = null, $format = null, EasyRdf\TypeMapperInterface $mapper = null) {
$this->mapper = $mapper ?? new EasyRdf\DefaultTypeMapper();
}
(...)
protected function classForResource($uri) {
$this->mapper::getClass($uri, $this->index[$uri]);
}
(...)
}
class myMapper implements EasyRdf\TypeMapperInterface {
private static $mappings = [(...whatever...)];
public static function getClass(string $uri, array $properties): string {
$class = (...guessing code goes here...);
return $class;
}
public static function defineMapping(string $rdfType, string $class) {
self::$mappings[$rdfType] = $class;
}
}
$g1 = new EasyRdf\Graph(null, null, nul, new myMapper());
$g2 = new EasyRdf\Graph(null, null, nul, new myMapper());
myMapper::defineMappings('foo', 'bar'); // applies both to $g1 and $g2!
Seems fine. But out of the scope of this issue, which is all about introducing a brand new type of mapping strategy (along the existing one, RDFtype-based).
My point is it rather does't make sense to hardcode another type mapping strategy into the EasyRdf. I think it would be better to modify EasyRdf in a way anyone can easily plug in any type mapping strategy (s)he wants (once they need a specific one).
And if someone thinks her/his particular type mapping strategy can be useful by others, (s)he can publish her/his type mapper as a composer package so others can easily reuse it.
In the approach I propose you would introduce the new type mapping strategy by developing your own class implementing the EasyRdf\TypeMapperInterface (which can extend or internally call EasyRdf\TypeMapper if you find it useful) and there is no need to incorporate your new mapping strategy with the EasyRdf code (which is for sure good from the long term code maintainability point of view).
Probably I choose the wrong terms, and "mapping strategy" was not the right one.
Graph needs some way to obtain, from TypeMapper, an information: which is the PHP class to use to instantiate new objects, through a known API. Right now, the only existing API provides the PHP class given a (set of) RDF type(s). Here is proposed to add a new API to provide a PHP class due a given URI and related set of properties.
The point is about the API to adopt, for which Graph must (of course) be aware. Then the discussion about implementing such API into a global hard coded static class, or a pluggable interface implementable by the user, for me is a different issue.
Ok. Just your code examples were focused on implementations and not on defining an API :-)
When it comes to the API I think my proposal of:
interface EasyRdf\TypeMapperInterface {
public static function getClass(string $uri, array $properties): string;
}
fits pretty well the second approach you proposed (with the interface).
Than:
- current
EasyRdf\TypeMapperwould need to get the compatiblegetClass()method with the type mapping code moved fromEasyRdf\Graph::classForResource():public static function getClass(string $uri, array $properties) { $rdfType = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type'; foreach ($properties[$rdfType] ?? [] as $type) { if ($type['type'] == 'uri' or $type['type'] == 'bnode') { $type = RdfNamespace::expand($type['value']); if (isset($this->map[$type])) { return $this->map[$type]; } } } // Parsers don't typically add a rdf:type to rdf:List, so we have to // do a bit of 'inference' here using properties. if ($uri == 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil' or isset($properties['http://www.w3.org/1999/02/22-rdf-syntax-ns#first']) or isset($properties['http://www.w3.org/1999/02/22-rdf-syntax-ns#rest']) ) { return 'EasyRdf\Collection'; } return null; } EasyRdf\Graphneeds to be able to determine which implementation ofEasyRdf\TypeMapperInterfaceit should use. This can be achieved either by adding additional parameter to the the graph constructor or by a separate setter method. Below an example with a constructor and a default mapper being set toEasyRdf\TypeMapperclass EasyRdf\Graph() { private $mapper; public function __construct($uri = null, $data = null, $format = null, EasyRdf\TypeMapperInterface $mapper = null) { (...) $this->mapper = $mapper ?? new EasyRdf\TypeMapper(); } (...) }EasyRdf\Graph::classForResource()can be simplified to:protected function classForResource($uri) { $this->mapper::getClass($uri, $this->index[$uri]); }- Anyone can implement their own implementation of the
EasyRdf\TypeMapperInterfaceand askEasyRdf\Graphto use it, e.g. (of course reusingasyRdf\TypeMapperis optional, one can implement a brand new type mapper as well)class myTypeMapper extends EasyRdf\TypeMapper { public static function getClass(string $uri, array $properties) { // my own type mapping code which should take precedense // over the basic type-based mapping provided by EasyRdf\TypeMapper // goes here $class = parent::getClass($uri, $properties); if ($class) { return $class; } // my own type mapping code once the basic type-based mapping provided by EasyRdf\TypeMapper failed // goes here } } $graph = new EasyRdf\Graph(null, null, null, new myTypeMapper());