I have created a custom Entity for blogs. I am not using nodes. The way my entity works is that all languages are stored in a single Entity. The only way to determine if a language has been submitted is if there is a title for that language. As the title is a field, the data for the titles is stores in {field_data_field_blog_title}. The way to determine whether an English version of the Entity in question exists, is to check if there is a row in the {field_data_field_blog_title} table for the current Entity ID and language.

So in hook_views_data_alter() I have added the following code:

function ba_views_data_alter(&$data)
{
    $data['j_blog']['language'] = array
    (
        'title' => t('Blog language'),
        'help' => t('The language of the blog'),
        'filter' => array
        (
            'handler' => 'views_handler_filter_ba_blog_language',
        ),
    );
}

And in my handler, views_handler_filter_ba_blog_language.inc, I have the following:


/**
* Filter by current_language (bundle)
*
* @ingroup views_filter_handlers
*/
class views_handler_filter_ba_blog_language extends views_handler_filter
{
    function value_form(&$form, &$form_state)
    {
        parent::value_form($form, $form_state);
       
        $languages = language_list();
        foreach($languages as $l)
        {
            $options[$l->language] = $l->name;
        }
        $options['current'] = t('Current language');
       
        $form['blog_language'] = array
        (
            '#type' => 'radios',
            '#title' => t('Language'),
            '#options' => $options,
        );
    }

    function value_submit($form, &$form_state)
    {
        $form_state['values']['options']['value'] = $form_state['values']['options']['blog_language'];
    }

    function query()
    {
        global $language;
       
        if (empty($this->value))
        {
            return;
        }
        $this->ensure_my_table();
       
        $value = ($this->value == 'current') ? $language->language : $this->value;

        // THIS IS WHERE I WANT TO ADD MY JOIN
    }
}

Basically, I want to force something like this in my query:
JOIN {field_data_field_blog_title} ON {field_data_field_blog_title}.id = blog.id AND {field_data_field_blog_title}.language = :language

By forcing a join where a row exists with the language, it will ensure that only blogs with the given language are returned.

The problem is that I don't know how to add my table with a join. Can someone give me some assistance with this?

Comments

absoludo’s picture

I have not read your question thouroughly, but you can try it with hook_views_query_alter().

You still have to work on this, but you will get something like this:

/**
 * Implements hook_views_query_alter().
 */
function YOUR_MODULE_views_query_alter(&$view, &$query) {
  if ($view->name == 'MY_VIEW') {
    // Determine language, not sure where to find this here
    $language = SOMEWHERE_YOU_MUST_DETERMINE_LANUAGE;

    // Join your tables
    $query->join('field_data_field_blog_title', 'fbt', 'fbt.id = blog.id AND fbt.language = ' . $language);
  }
}
Jaypan’s picture

Thanks for the suggestion, and I think I'm on the right track, but unfortunately join() isn't a method on the current class for the $query here, as I get the following error:

Fatal error: Call to undefined method views_plugin_query_default::join()

Any idea how I can add a join on to the query at this point?

absoludo’s picture

I always mix up the views queries with dynamic queries.

Fortunatelly I did do a similair join for a project once:

  // Write the join conditions
  $join = new views_join();
  $join->table = 'field_data_field_blog_title';
  $join->left_table = 'blog';// or wathever table you want to join with
  $join->field = 'id';
  $join->left_field = 'id';// or wathever column you want to join with
  $join->type = 'LEFT';// if you want another join type play with this value

  // Do the actual join
  $query->table_queue['blog_title'] = array (
    'alias' => 'blog_title',// I believe this is optional
    'table' => 'field_data_field_blog_title',
    'relationship' => 'blog',
    'join' => $join,
  );
  $query->where[] = array(
    'conditions' => array(
      array(
        'field' => 'blog_title.language',
        'value' => $language,
        'operator' => '=',
      ),
      // add more conditions if you want to
    ),
    //'type' => 'OR' ,// I believe this is an operator for multiple conditions
  );
ann b’s picture

Thank you. I didn't know the database api methods won't work in this hook. This worked perfectly when I needed to sort a workbench view query programmatically.

yhuet’s picture

I dont think you should add a join there. I believe at this point your table is already joined and has an alias.

Maybe you can do something like this in ba_views_data_alter() :

    $data['j_blog']['table']['join'] = array(
        'blog' => array(
            'left_field' => 'id',
            'field' => 'id',
            'extra' => array(
                array('field' => 'language', 'value' => '***CURRENT_LANGUAGE***'),
            ),
        ),
    );

I hope this helps.

Jaypan’s picture

Thank you for your assistance, but maybe I wasn't clear on my reasoning for doing the join there. The join itself is a bit of a tricky way of forcing a conditional. I don't want my table to always be joined, on the contrary, I will only want it to join if there is a value for the current language.

I think hook_views_query_alter() suggested by absoludo (thanks!) may solve my problem. I'll look at that when I get to the office tomorrow.

mortona2k’s picture

I'm working on a similar solution.

I've added a relationship to a field on field_collection item, using hook_data_alter. That allows me to add the relationship to my view. I can throw in 'extra' = "AND field_name ='whatever'" which gives me the exact sql query i'm looking for.

But I need to add this extra to the join dynamically, based on options set when adding the relationship to the view.
Use pre_query to modify $this->definition and add the extra.

Now I just need to define the options form and use those values.

The other Andrew Morton

Jaypan’s picture

Nice, this is an issue I have to fix within the next week, so I'll work with that and see if we can't help each other come to a solution on it.

bucefal91’s picture

Hi, guys!

I had to do INNER JOIN in my sort handler and while searching in google I got to this thread. I used the info from comments above to compile my solution which I am gonna share in a second. On a different note, I'd like to say that achieving it via hook_views_query_alter() from my point of view is a bit inprofessional. For example in my case I am building a general purpose module that will be used in many different occasions and of course I can't forsee all possible use cases in hook_views_query_alter(). So...

$table = 'table_i_wanna_join'; // i.e. how the table is represented in drupal database schema, without prefixes.
$left_table = $this->table; // the table to which I want to join it. Mostly I assume you'll want to join it to the table of your handler
$left_field = 'left_id'; // a column in $left_table on which you wanna join
$field = 'id'; // a column in $table on which you wanna join
// We create a new join object and provide it with information about the join we wanna make.
$join = new views_join();
$join->construct($table, $left_table, $left_field, $field, array(), 'INNER');

// Now, here we've kind of compiled the relationship between 2 tables, yet we gotta throw it into the View to take effect.

$alias = 'alias_i_want_for_table'; // Alias under which you wanna see $table once it has been JOINed with all the other tables.
// $this->table_alias below exposes to view under what alias $left_table is manipulated in the SQL we are building.
// $this->relationship below is used when the view has relationships and the same table may be referred for example from the base table of view, or from one of the relationships.
$alias = $this->query->add_relationship($alias, $join, $this->table_alias, $this->relationship);
// Last note, you gotta get $alias as return of this method because the desired alias may be already occupied by some other previously joined table, so this method will kindly adjust your desired alias to some unique string based on the original value of $alias.

Above is a general case. Jaypan, as far as I got it from your post, you will need to use parameter $extra in views_join::construct() method to pass on the language restriction, and your left_field and field will be the IDs of your entities. It took me the whole evening to figure it out, so I decided it'd be worth to post it somewhere :)

http://takearest.ru - my Drupal baby :)

Jaypan’s picture

Thanks for posting - I still haven't resolved this problem, and it's on the back burner, but something that needs to be fixed. I'll look closely at your post and see if I can get it to work now. Thanks!

Jaypan’s picture

Thank you again for the code, but you did not give the context in which it is used. It would appear you are not doing so in hook_views_query_alter(), so where is it? Have you written a custom handler? What function is this that we are looking at?

bucefal91’s picture

Yes, the snippet I showed should be from a custom handler's query() method. I thought to include this informaiton into my comment, but now I don't see edit link, so I guess I can't fix it :(

I read your 1st post in this thread, basically you should use my spinnet right where you had this comment:

// THIS IS WHERE I WANT TO ADD MY JOIN

http://takearest.ru - my Drupal baby :)

Jaypan’s picture

Thank you bucafel91, much appreciated.

Jaypan’s picture

With your help, I was able to come up with a solution that works. It's not my ideal solution, but it works.

What I wanted to was join the blog title table on two fields, the ID and the language. This would effectively filter blogs by the given languages. Unfortunately, I couldn't figure out how to join on two fields, so in the end I used a condition (WHERE) to choose only blogs that have a title in the second language. This means the query won't be as fast as if I were joining on both fields with no WHERE statement, but I could not figure out how to join on multiple fields, so I am making do with what I have. It's really just academic, as I am only selecting 10 results, so the difference is negligible. That said, if someone else knows how to join a table on multiple fields, please feel free to explain!


/**
* Filter by current_language (bundle)
*
* @ingroup views_filter_handlers
*
*/
class views_handler_filter_ba_blog_language extends views_handler_filter
{
	function value_form(&$form, &$form_state)
	{
		parent::value_form($form, $form_state);

		$view = $form_state['view'];
		$keys = array();
		foreach($view as $k => $v)
		{
			$keys[] = $k;
		}

		$languages = language_list();
		foreach($languages as $l)
		{
			$options[$l->language] = $l->name;
		}
		$options['current'] = t('Current language');

		$form['blog_language'] = array
		(
			'#type' => 'radios',
			'#title' => t('Language'),
			'#options' => $options,
			'#default_value' => $form_state['values']['blog_language'],
		);
	}

	function value_submit($form, &$form_state)
	{
		$form_state['values']['options']['value'] = $form_state['values']['options']['blog_language'];
	}

	function query()
	{
		global $language;
		
		// If no value has been given, then we don't need to do anything.
		if(empty($this->value))
		{
			return;
		}

		$this->ensure_my_table();
		
        // Determine the language on which to filter. If the given value is 'current'
        // Then we use the current language. Otherwise we use the provided language 
        $selected_language = ($this->value == 'current') ? $language->language : $this->value;

		$table = 'field_data_field_blog_title'; // The table to be joined
		$left_table = $this->table; // the table to be joined to
		$left_field = 'jeid'; // a column in $left_table on which to join
		$field = 'entity_id'; // a column in $table on which to join
		// We create a new join object and provide it with information about the join
		$join = new views_join();
		$join->construct($table, $left_table, $left_field, $field, array('blog_title_language.language' => $selected_language), 'INNER');

		// Add a condition so that only items with the selected langauge are shown
		$this->query->add_where(0, 'blog_title_language.language', $selected_language, '=');
	}
}
bucefal91’s picture

I am glad you were able to achieve it! :)

Last note, on the additional conditions for JOIN. The argument $extra for method $join->construct(). Easiest case is to provide a string there. Then your JOIN will be:
JOIN .... ON left_table.left_field = table.field AND ($your_extra_string)
If you want to go a bit more heuristic, or if alias of either table is not yet known to you, first of all study the code of views_join::build_join() to see how it handles $extra. But generally speaking the structure should be something like the following:

$extra = array();
$extra[] = array(
  'table' => 'table_alias_here',
  'field' => 'field_name_here',
  'value' => 1,
  'operator' => '>',
);
// and so on, as many times, as many extra conditions you need.

http://takearest.ru - my Drupal baby :)

Jaypan’s picture

Thank you bucefal! I was trying to get 'extra' to work last night, but I was passing it a string.

derekrezek’s picture


class views_handler_filter_inbound_calling_filter_source extends views_handler_filter {

  function value_form(&$form, &$form_state) {
    parent::value_form($form, $form_state);
    if (!empty($form_state['exposed'])) {
      $form['value'] = array(
        '#type' => 'select',
        '#title' => t('Select Source'),
        '#options' => inbound_calling_source_options(),
      );
    }
//    else {
//      $form['value'] = array(
//        '#type' => 'hidden',
//        '#value' => $this->options['filter_default_value'],
//      );
//    }
  }

  function query() {
    $this->ensure_my_table();
    if (is_array($this->value)) {
      $value = array_filter($this->value);
    }
    else {
      $value = $this->value;
    }
    $table = 'inbound_calling_complain_source'; // The table to be joined
    $left_table = $this->table; // the table to be joined to
    $left_field = $this->real_field; // a column in $left_table on which to join
    $field = 'ibcl_hid'; // a column in $table on which to join
    // We create a new join object and provide it with information about the join
    $join = new views_join();
    $extra = array();
    $extra[] = array(
      'table' => 'alias_i_want_for_table',
      'field' => 'source',
      'value' => $value,
      'operator' => '=',
    );
    $join->construct($table, $left_table, $left_field, $field, $extra, 'INNER');
    $alias = 'alias_i_want_for_table';
    $alias = $this->query->add_relationship($alias, $join, $this->table_alias, $this->relationship);

//    $this->query->add_where(0, 'alias_i_want_for_table.source', $value, '='); 
// this where clause was working, but wanted to add the AND in the join itself to improve efficiency, so after adding the $extra, i commented it out.

  }

}

derekrezek’s picture

INNER JOIN {inbound_calling_complain_source} alias_i_want_for_table ON inbound_calling_calllog_header.ibcl_hid = alias_i_want_for_table.ibcl_hid AND alias_i_want_for_table.source = '2'

here 2 is the dynamic value assigned to $value