tag:blogger.com,1999:blog-4937488729022245645.post-18776513251723647632008-07-17T12:45:00.002-05:002008-07-17T12:47:03.726-05:002008-07-17T12:47:03.726-05:00callback chains and metaclasses<p>For whatever reason (don't ask) our Rails app needed to be able to define callbacks on objects. As it stands now, you can only define callbacks on classes. Well, metaclasses are classes that pertain to a single specific object, so I figured I could add the callbacks there and everything will work.</p>
<p>Please excuse my <em>extremely</em> contrived example.</p>
<pre class="textmate-source mac_classic"><span class="source source_ruby">p <span class="keyword keyword_operator keyword_operator_assignment keyword_operator_assignment_ruby">=</span> <span class="support support_class support_class_ruby">Post</span><span class="punctuation punctuation_separator punctuation_separator_method punctuation_separator_method_ruby">.</span>find<span class="punctuation punctuation_section punctuation_section_function punctuation_section_function_ruby">(</span><span class="punctuation punctuation_separator punctuation_separator_method punctuation_separator_method_ruby">...</span><span class="punctuation punctuation_section punctuation_section_function punctuation_section_function_ruby">)</span>
<span class="keyword keyword_control keyword_control_ruby">if</span> p<span class="punctuation punctuation_separator punctuation_separator_method punctuation_separator_method_ruby">.</span>contains_porn?
<span class="meta meta_class meta_class_ruby"> <span class="keyword keyword_control keyword_control_class keyword_control_class_ruby">class</span> <span class="entity entity_name entity_name_type entity_name_type_class entity_name_type_class_ruby"><span class="variable variable_other variable_other_object variable_other_object_ruby"><span class="punctuation punctuation_definition punctuation_definition_variable punctuation_definition_variable_ruby"><<</span> p</span></span></span>
before_save <span class="constant constant_other constant_other_symbol constant_other_symbol_ruby"><span class="punctuation punctuation_definition punctuation_definition_constant punctuation_definition_constant_ruby">:</span>filter_content</span>
<span class="keyword keyword_control keyword_control_ruby">end</span>
<span class="keyword keyword_control keyword_control_ruby">end</span>
p<span class="punctuation punctuation_separator punctuation_separator_method punctuation_separator_method_ruby">.</span>save <span class="comment comment_line comment_line_number-sign comment_line_number-sign_ruby"><span class="punctuation punctuation_definition punctuation_definition_comment punctuation_definition_comment_ruby">#</span> should trigger filter_content</span></span></pre>
<p>Well it doesn't work.</p>
<p>After digging through the ActiveSupport source code, I saw that it only looks for callback chains in the object's class, not metaclass. That's not too hard to fix...</p>
<pre class="textmate-source mac_classic"><span class="source source_ruby"><span class="meta meta_module meta_module_ruby"><span class="keyword keyword_control keyword_control_module keyword_control_module_ruby">module</span> <span class="entity entity_name entity_name_type entity_name_type_module entity_name_type_module_ruby">ActiveSupport</span></span>
<span class="meta meta_module meta_module_ruby"> <span class="keyword keyword_control keyword_control_module keyword_control_module_ruby">module</span> <span class="entity entity_name entity_name_type entity_name_type_module entity_name_type_module_ruby">Callbacks</span></span>
<span class="meta meta_function meta_function_method meta_function_method_with-arguments meta_function_method_with-arguments_ruby"><span class="keyword keyword_control keyword_control_def keyword_control_def_ruby">def</span> <span class="entity entity_name entity_name_function entity_name_function_ruby">run_callbacks</span><span class="punctuation punctuation_definition punctuation_definition_parameters punctuation_definition_parameters_ruby">(</span><span class="variable variable_parameter variable_parameter_function variable_parameter_function_ruby">kind<span class="punctuation punctuation_separator punctuation_separator_object punctuation_separator_object_ruby">,</span> options <span class="keyword keyword_operator keyword_operator_assignment keyword_operator_assignment_ruby">=</span> <span class="punctuation punctuation_section punctuation_section_scope punctuation_section_scope_ruby">{}</span><span class="punctuation punctuation_separator punctuation_separator_object punctuation_separator_object_ruby">,</span> <span class="keyword keyword_operator keyword_operator_arithmetic keyword_operator_arithmetic_ruby">&</span>block</span><span class="punctuation punctuation_definition punctuation_definition_parameters punctuation_definition_parameters_ruby">)</span></span>
callback_chain_method <span class="keyword keyword_operator keyword_operator_assignment keyword_operator_assignment_ruby">=</span> <span class="string string_quoted string_quoted_double string_quoted_double_ruby"><span class="punctuation punctuation_definition punctuation_definition_string punctuation_definition_string_begin punctuation_definition_string_begin_ruby">"</span><span class="source source_ruby source_ruby_embedded source_ruby_embedded_source"><span class="punctuation punctuation_section punctuation_section_embedded punctuation_section_embedded_ruby">#{</span>kind<span class="punctuation punctuation_section punctuation_section_embedded punctuation_section_embedded_ruby">}</span></span>_callback_chain<span class="punctuation punctuation_definition punctuation_definition_string punctuation_definition_string_end punctuation_definition_string_end_ruby">"</span></span>
<span class="comment comment_line comment_line_number-sign comment_line_number-sign_ruby"> <span class="punctuation punctuation_definition punctuation_definition_comment punctuation_definition_comment_ruby">#</span> Meta class inherits Class so we don't have to merge it in 1.9
</span> <span class="keyword keyword_control keyword_control_ruby">if</span> <span class="variable variable_other variable_other_constant variable_other_constant_ruby">RUBY_VERSION</span> <span class="keyword keyword_operator keyword_operator_comparison keyword_operator_comparison_ruby">>=</span> <span class="string string_quoted string_quoted_single string_quoted_single_ruby"><span class="punctuation punctuation_definition punctuation_definition_string punctuation_definition_string_begin punctuation_definition_string_begin_ruby">'</span>1.9<span class="punctuation punctuation_definition punctuation_definition_string punctuation_definition_string_end punctuation_definition_string_end_ruby">'</span></span>
metaclass<span class="punctuation punctuation_separator punctuation_separator_method punctuation_separator_method_ruby">.</span>send<span class="punctuation punctuation_section punctuation_section_function punctuation_section_function_ruby">(</span>callback_chain_method<span class="punctuation punctuation_section punctuation_section_function punctuation_section_function_ruby">)</span><span class="punctuation punctuation_separator punctuation_separator_method punctuation_separator_method_ruby">.</span>run<span class="punctuation punctuation_section punctuation_section_function punctuation_section_function_ruby">(</span><span class="variable variable_language variable_language_ruby">self</span><span class="punctuation punctuation_separator punctuation_separator_object punctuation_separator_object_ruby">,</span> options<span class="punctuation punctuation_separator punctuation_separator_object punctuation_separator_object_ruby">,</span> <span class="keyword keyword_operator keyword_operator_arithmetic keyword_operator_arithmetic_ruby">&</span>block<span class="punctuation punctuation_section punctuation_section_function punctuation_section_function_ruby">)</span>
<span class="keyword keyword_control keyword_control_ruby">else</span>
callbacks <span class="keyword keyword_operator keyword_operator_assignment keyword_operator_assignment_ruby">=</span> <span class="variable variable_language variable_language_ruby">self</span><span class="punctuation punctuation_separator punctuation_separator_method punctuation_separator_method_ruby">.</span>class<span class="punctuation punctuation_separator punctuation_separator_method punctuation_separator_method_ruby">.</span>send<span class="punctuation punctuation_section punctuation_section_function punctuation_section_function_ruby">(</span>callback_chain_method<span class="punctuation punctuation_section punctuation_section_function punctuation_section_function_ruby">)</span> <span class="keyword keyword_operator keyword_operator_other keyword_operator_other_ruby">|</span> metaclass<span class="punctuation punctuation_separator punctuation_separator_method punctuation_separator_method_ruby">.</span>send<span class="punctuation punctuation_section punctuation_section_function punctuation_section_function_ruby">(</span>callback_chain_method<span class="punctuation punctuation_section punctuation_section_function punctuation_section_function_ruby">)</span>
callbacks<span class="punctuation punctuation_separator punctuation_separator_method punctuation_separator_method_ruby">.</span>run<span class="punctuation punctuation_section punctuation_section_function punctuation_section_function_ruby">(</span><span class="variable variable_language variable_language_ruby">self</span><span class="punctuation punctuation_separator punctuation_separator_object punctuation_separator_object_ruby">,</span> options<span class="punctuation punctuation_separator punctuation_separator_object punctuation_separator_object_ruby">,</span> <span class="keyword keyword_operator keyword_operator_arithmetic keyword_operator_arithmetic_ruby">&</span>block<span class="punctuation punctuation_section punctuation_section_function punctuation_section_function_ruby">)</span>
<span class="keyword keyword_control keyword_control_ruby">end</span>
<span class="keyword keyword_control keyword_control_ruby">end</span>
<span class="keyword keyword_control keyword_control_ruby">end</span>
<span class="keyword keyword_control keyword_control_ruby">end</span></span></pre>
<p>Credit to Josh Peek for the Ruby 1.9 fix</p>
<p>Ok, so that got it working... with <strong>one major caveat: you cannot serialize objects that use callbacks</strong>. Ruby cannot serialize objects that have metaclasses and the <tt>run_callbacks</tt> method creates a metaclass whether you use it or not.</p>
<p>That's fine with me, I don't really like serializing objects anyways, but apparently it's a major problem for other people...</p>
<p>
<a href="http://github.com/rails/rails/commit/e0846c8417093853f4f7f62732983e990c28d669">http://github.com/rails/rails/commit/e0846c8417093853f4f7f62732983e990c28d669</a>
<br/>
<a href="http://rails.lighthouseapp.com/projects/8994/tickets/575-callbacks-don-t-work-from-extended-modules">http://rails.lighthouseapp.com/projects/8994/tickets/575-callbacks-don-t-work-from-extended-modules</a>
</p>
<p>Josh Peek suggested a solution where we store the callback chains on the objects themselves, so object specific callbacks would be done like this...</p>
<pre class="textmate-source mac_classic"><span class="source source_ruby">p <span class="keyword keyword_operator keyword_operator_assignment keyword_operator_assignment_ruby">=</span> <span class="support support_class support_class_ruby">Post</span><span class="punctuation punctuation_separator punctuation_separator_method punctuation_separator_method_ruby">.</span>find<span class="punctuation punctuation_section punctuation_section_function punctuation_section_function_ruby">(</span><span class="punctuation punctuation_separator punctuation_separator_method punctuation_separator_method_ruby">...</span><span class="punctuation punctuation_section punctuation_section_function punctuation_section_function_ruby">)</span>
p<span class="punctuation punctuation_separator punctuation_separator_method punctuation_separator_method_ruby">.</span>before_save <span class="constant constant_other constant_other_symbol constant_other_symbol_ruby"><span class="punctuation punctuation_definition punctuation_definition_constant punctuation_definition_constant_ruby">:</span>filter_content</span> <span class="keyword keyword_control keyword_control_ruby">if</span> p<span class="punctuation punctuation_separator punctuation_separator_method punctuation_separator_method_ruby">.</span>contains_porn?
p<span class="punctuation punctuation_separator punctuation_separator_method punctuation_separator_method_ruby">.</span>save <span class="comment comment_line comment_line_number-sign comment_line_number-sign_ruby"><span class="punctuation punctuation_definition punctuation_definition_comment punctuation_definition_comment_ruby">#</span> should trigger filter_content</span></span></pre>
<p>That seems like a very simple, clean and rational solution. I'll look into coding it up later.</p>Christopher J. Bottarohttp://www.blogger.com/profile/14116593743589959438noreply@blogger.com